Browse Source

Add default operations for OPTIONS method in REST

It defaults to setting the Allow header to "HEAD, GET, OPTIONS".
Loïc Hoguin 12 years ago
parent
commit
b58a0549e1
4 changed files with 45 additions and 12 deletions
  1. 1 1
      guide/rest_handlers.md
  2. 29 11
      src/cowboy_rest.erl
  3. 10 0
      test/http_SUITE.erl
  4. 5 0
      test/rest_empty_resource.erl

+ 1 - 1
guide/rest_handlers.md

@@ -77,7 +77,7 @@ empty column means there is no default value for this callback.
 | moved_permanently      | `false`                   |
 | moved_permanently      | `false`                   |
 | moved_temporarily      | `false`                   |
 | moved_temporarily      | `false`                   |
 | multiple_choices       | `false`                   |
 | multiple_choices       | `false`                   |
-| options                |                           |
+| options                | `ok`                      |
 | previously_existed     | `false`                   |
 | previously_existed     | `false`                   |
 | resource_exists        | `true`                    |
 | resource_exists        | `true`                    |
 | service_available      | `true`                    |
 | service_available      | `true`                    |

+ 29 - 11
src/cowboy_rest.erl

@@ -31,6 +31,9 @@
 	handler :: atom(),
 	handler :: atom(),
 	handler_state :: any(),
 	handler_state :: any(),
 
 
+	%% Allowed methods. Only used for OPTIONS requests.
+	allowed_methods :: [binary()],
+
 	%% Media type.
 	%% Media type.
 	content_types_p = [] ::
 	content_types_p = [] ::
 		[{binary() | {binary(), binary(), [{binary(), binary()}] | '*'},
 		[{binary() | {binary(), binary(), [{binary(), binary()}] | '*'},
@@ -119,32 +122,43 @@ allowed_methods(Req, State=#state{method=Method}) ->
 	case call(Req, State, allowed_methods) of
 	case call(Req, State, allowed_methods) of
 		no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> ->
 		no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> ->
 			next(Req, State, fun malformed_request/2);
 			next(Req, State, fun malformed_request/2);
+		no_call when Method =:= <<"OPTIONS">> ->
+			next(Req, State#state{allowed_methods=
+				[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]},
+				fun malformed_request/2);
 		no_call ->
 		no_call ->
-			method_not_allowed(Req, State, [<<"GET">>, <<"HEAD">>]);
+			method_not_allowed(Req, State,
+				[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]);
 		{halt, Req2, HandlerState} ->
 		{halt, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
 		{List, Req2, HandlerState} ->
 		{List, Req2, HandlerState} ->
 			State2 = State#state{handler_state=HandlerState},
 			State2 = State#state{handler_state=HandlerState},
 			case lists:member(Method, List) of
 			case lists:member(Method, List) of
-				true -> next(Req2, State2, fun malformed_request/2);
-				false -> method_not_allowed(Req2, State2, List)
+				true when Method =:= <<"OPTIONS">> ->
+					next(Req2, State2#state{allowed_methods=
+						[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]},
+						fun malformed_request/2);
+				true ->
+					next(Req2, State2, fun malformed_request/2);
+				false ->
+					method_not_allowed(Req2, State2, List)
 			end
 			end
 	end.
 	end.
 
 
 method_not_allowed(Req, State, Methods) ->
 method_not_allowed(Req, State, Methods) ->
 	Req2 = cowboy_req:set_resp_header(
 	Req2 = cowboy_req:set_resp_header(
-		<<"allow">>, method_not_allowed_build(Methods, []), Req),
+		<<"allow">>, build_allow_header(Methods, []), Req),
 	respond(Req2, State, 405).
 	respond(Req2, State, 405).
 
 
-method_not_allowed_build([], []) ->
+build_allow_header([], []) ->
 	<<>>;
 	<<>>;
-method_not_allowed_build([], [_Ignore|Acc]) ->
+build_allow_header([], [_Ignore|Acc]) ->
 	lists:reverse(Acc);
 	lists:reverse(Acc);
-method_not_allowed_build([Method|Tail], Acc) when is_atom(Method) ->
+build_allow_header([Method|Tail], Acc) when is_atom(Method) ->
 	Method2 = list_to_binary(atom_to_list(Method)),
 	Method2 = list_to_binary(atom_to_list(Method)),
-	method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]);
-method_not_allowed_build([Method|Tail], Acc) ->
-	method_not_allowed_build(Tail, [<<", ">>, Method|Acc]).
+	build_allow_header(Tail, [<<", ">>, Method2|Acc]);
+build_allow_header([Method|Tail], Acc) ->
+	build_allow_header(Tail, [<<", ">>, Method|Acc]).
 
 
 malformed_request(Req, State) ->
 malformed_request(Req, State) ->
 	expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
 	expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
@@ -180,8 +194,12 @@ valid_entity_length(Req, State) ->
 
 
 %% If you need to add additional headers to the response at this point,
 %% If you need to add additional headers to the response at this point,
 %% you should do it directly in the options/2 call using set_resp_headers.
 %% you should do it directly in the options/2 call using set_resp_headers.
-options(Req, State=#state{method= <<"OPTIONS">>}) ->
+options(Req, State=#state{allowed_methods=Allow, method= <<"OPTIONS">>}) ->
 	case call(Req, State, options) of
 	case call(Req, State, options) of
+		no_call ->
+			Req2 = cowboy_req:set_resp_header(<<"allow">>,
+				build_allow_header(Allow, []), Req),
+			respond(Req2, State, 200);
 		{halt, Req2, HandlerState} ->
 		{halt, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
 		{ok, Req2, HandlerState} ->
 		{ok, Req2, HandlerState} ->

+ 10 - 0
test/http_SUITE.erl

@@ -61,6 +61,7 @@
 -export([rest_missing_get_callbacks/1]).
 -export([rest_missing_get_callbacks/1]).
 -export([rest_missing_put_callbacks/1]).
 -export([rest_missing_put_callbacks/1]).
 -export([rest_nodelete/1]).
 -export([rest_nodelete/1]).
+-export([rest_options_default/1]).
 -export([rest_param_all/1]).
 -export([rest_param_all/1]).
 -export([rest_patch/1]).
 -export([rest_patch/1]).
 -export([rest_resource_etags/1]).
 -export([rest_resource_etags/1]).
@@ -131,6 +132,7 @@ groups() ->
 		rest_missing_get_callbacks,
 		rest_missing_get_callbacks,
 		rest_missing_put_callbacks,
 		rest_missing_put_callbacks,
 		rest_nodelete,
 		rest_nodelete,
+		rest_options_default,
 		rest_param_all,
 		rest_param_all,
 		rest_patch,
 		rest_patch,
 		rest_resource_etags,
 		rest_resource_etags,
@@ -367,6 +369,7 @@ init_dispatch(Config) ->
 			{"/patch", rest_patch_resource, []},
 			{"/patch", rest_patch_resource, []},
 			{"/resetags", rest_resource_etags, []},
 			{"/resetags", rest_resource_etags, []},
 			{"/rest_expires", rest_expires, []},
 			{"/rest_expires", rest_expires, []},
+			{"/rest_empty_resource", rest_empty_resource, []},
 			{"/loop_recv", http_handler_loop_recv, []},
 			{"/loop_recv", http_handler_loop_recv, []},
 			{"/loop_timeout", http_handler_loop_timeout, []},
 			{"/loop_timeout", http_handler_loop_timeout, []},
 			{"/", http_handler, []}
 			{"/", http_handler, []}
@@ -967,6 +970,13 @@ rest_nodelete(Config) ->
 		build_url("/nodelete", Config), Client),
 		build_url("/nodelete", Config), Client),
 	{ok, 500, _, _} = cowboy_client:response(Client2).
 	{ok, 500, _, _} = cowboy_client:response(Client2).
 
 
+rest_options_default(Config) ->
+	Client = ?config(client, Config),
+	{ok, Client2} = cowboy_client:request(<<"OPTIONS">>,
+		build_url("/rest_empty_resource", Config), Client),
+	{ok, 200, Headers, _} = cowboy_client:response(Client2),
+	{_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers).
+
 rest_patch(Config) ->
 rest_patch(Config) ->
 	Tests = [
 	Tests = [
 		{204, [{<<"content-type">>, <<"text/plain">>}], <<"whatever">>},
 		{204, [{<<"content-type">>, <<"text/plain">>}], <<"whatever">>},

+ 5 - 0
test/rest_empty_resource.erl

@@ -0,0 +1,5 @@
+-module(rest_empty_resource).
+-export([init/3]).
+
+init(_Transport, _Req, _Opts) ->
+	{upgrade, protocol, cowboy_rest}.