diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 003e5f9da..34aa24f08 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -246,9 +246,6 @@ handler :: atom(), handler_state :: any(), - %% Allowed methods. Only used for OPTIONS requests. - allowed_methods :: [binary()] | undefined, - %% Media type. content_types_p = [] :: [{binary() | {binary(), binary(), [{binary(), binary()}] | '*'}, @@ -324,41 +321,43 @@ known_methods(Req, State=#state{method=Method}) -> uri_too_long(Req, State) -> expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414). +stringify_allowed_methods(MethodList) when is_list(MethodList) -> + case MethodList of + [] -> <<>>; + _ -> + << ", ", Allow/binary >> = << << ", ", M/binary >> || M <- MethodList >>, + Allow + end. + %% allowed_methods/2 should return a list of binary methods. allowed_methods(Req, State=#state{method=Method}) -> + DefaultAllowedMethods = [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>], case call(Req, State, allowed_methods) of - no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> - 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 when Method =:= <<"HEAD">>; Method =:= <<"GET">>; Method =:= <<"OPTIONS">> -> + Allow = stringify_allowed_methods(DefaultAllowedMethods), + Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req), + next(Req2, State, fun malformed_request/2); no_call -> - method_not_allowed(Req, State, - [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]); + Allow = stringify_allowed_methods(DefaultAllowedMethods), + Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req), + method_not_allowed(Req2, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {List, Req2, State2} -> + Allow = stringify_allowed_methods(List), + Req3 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req2), case lists:member(Method, List) of - true when Method =:= <<"OPTIONS">> -> - next(Req2, State2#state{allowed_methods=List}, - fun malformed_request/2); true -> - next(Req2, State2, fun malformed_request/2); + next(Req3, State2, fun malformed_request/2); false -> - method_not_allowed(Req2, State2, List) + method_not_allowed(Req3, State2) end end. -method_not_allowed(Req, State, []) -> - Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req), - respond(Req2, State, 405); -method_not_allowed(Req, State, Methods) -> - << ", ", Allow/binary >> = << << ", ", M/binary >> || M <- Methods >>, - Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req), - respond(Req2, State, 405). +method_not_allowed(Req, State) -> + respond(Req, State, 405). malformed_request(Req, State) -> expect(Req, State, malformed_request, false, fun is_authorized/2, 400). @@ -413,16 +412,10 @@ valid_entity_length(Req, State) -> %% 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. -options(Req, State=#state{allowed_methods=Methods, method= <<"OPTIONS">>}) -> +options(Req, State=#state{method= <<"OPTIONS">>}) -> case call(Req, State, options) of - no_call when Methods =:= [] -> - Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req), - respond(Req2, State, 200); no_call -> - << ", ", Allow/binary >> - = << << ", ", M/binary >> || M <- Methods >>, - Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req), - respond(Req2, State, 200); + respond(Req, State, 200); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl index 510098f1e..d8998e9a1 100644 --- a/test/rest_handler_SUITE.erl +++ b/test/rest_handler_SUITE.erl @@ -472,7 +472,8 @@ delete_resource_missing(Config) -> Ref = gun:delete(ConnPid, "/delete_resource?missing", [ {<<"accept-encoding">>, <<"gzip">>} ]), - {response, _, 500, _} = gun:await(ConnPid, Ref), + {response, _, 500, Headers} = gun:await(ConnPid, Ref), + {_, <<"DELETE">>} = lists:keyfind(<<"allow">>, 1, Headers), ok. create_resource_created(Config) -> @@ -483,7 +484,8 @@ create_resource_created(Config) -> Ref = gun:post(ConnPid, "/create_resource?created", [ {<<"content-type">>, <<"application/text">>} ], <<"hello">>, #{}), - {response, _, 201, _} = gun:await(ConnPid, Ref), + {response, _, 201, Headers} = gun:await(ConnPid, Ref), + {_, <<"POST">>} = lists:keyfind(<<"allow">>, 1, Headers), ok. create_resource_see_other(Config) -> @@ -496,6 +498,7 @@ create_resource_see_other(Config) -> ], <<"hello">>, #{}), {response, _, 303, RespHeaders} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"location">>, 1, RespHeaders), + {_, <<"POST">>} = lists:keyfind(<<"allow">>, 1, RespHeaders), ok. error_on_malformed_accept(Config) -> @@ -778,6 +781,17 @@ last_modified_missing(Config) -> false = lists:keyfind(<<"last-modified">>, 1, Headers), ok. +head_call(Config) -> + doc("A successful HEAD request to a simple handler results in " + "a 200 OK response with the allow header set. (RFC7231 4.3.7)"), + ConnPid = gun_open(Config), + Ref = gun:head(ConnPid, "/", [ + {<<"accept-encoding">>, <<"gzip">>} + ]), + {response, fin, 200, Headers} = gun:await(ConnPid, Ref), + {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers), + ok. + options_missing(Config) -> doc("A successful OPTIONS request to a simple handler results in " "a 200 OK response with the allow header set. (RFC7231 4.3.7)"), @@ -799,6 +813,7 @@ provide_callback(Config) -> ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), + {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers), {ok, <<"This is REST!">>} = gun:await_body(ConnPid, Ref), ok.