0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

AcceptCallback may now return created/see_other tuples for POST

They replace and deprecate the {true,URI} return value.
This commit is contained in:
Martin Björklund 2020-09-11 12:35:36 +02:00 committed by Loïc Hoguin
parent 63a6b86fba
commit 8795233c57
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
4 changed files with 73 additions and 4 deletions

View file

@ -86,7 +86,10 @@ normal::
---- ----
AcceptCallback(Req, State) -> {Result, Req, State} AcceptCallback(Req, State) -> {Result, Req, State}
Result :: true | {true, URI :: iodata()} | false} Result :: true
| {created, URI :: iodata()}
| {see_other, URI :: iodata()}
| false
Default - crash Default - crash
---- ----
@ -99,11 +102,14 @@ For PUT requests, the body is a representation of the resource
that is being created or replaced. that is being created or replaced.
For POST requests, the body is typically application-specific For POST requests, the body is typically application-specific
instructions on how to process the request, but it may also instructions on how to process the request, but it may also be a
be a representation of the resource. When creating a new representation of the resource. When creating a new resource with POST
resource with POST at a different location, return `{true, URI}` at a different location, return `{created, URI}` or `{see_other, URI}`
with `URI` the new location. with `URI` the new location.
The `see_other` tuple will redirect the client to the new location
automatically.
For PATCH requests, the body is a series of instructions on For PATCH requests, the body is a series of instructions on
how to update the resource. Patch files or JSON Patch are how to update the resource. Patch files or JSON Patch are
examples of such media types. examples of such media types.
@ -724,6 +730,9 @@ listed here, like the authorization header.
== Changelog == Changelog
* *2.9*: An `AcceptCallback` can now return `{created, URI}` or
`{see_other, URI}`. The return value `{true, URI}`
is deprecated.
* *2.7*: The media type wildcard in `content_types_accepted` * *2.7*: The media type wildcard in `content_types_accepted`
is now documented. is now documented.
* *2.6*: The callback `rate_limited` was added. * *2.6*: The callback `rate_limited` was added.

View file

@ -1104,6 +1104,14 @@ process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) ->
next(Req2, State2, fun maybe_created/2); next(Req2, State2, fun maybe_created/2);
{false, Req2, State2} -> {false, Req2, State2} ->
respond(Req2, State2, 400); respond(Req2, State2, 400);
{{created, ResURL}, Req2, State2} when Method =:= <<"POST">> ->
Req3 = cowboy_req:set_resp_header(
<<"location">>, ResURL, Req2),
respond(Req3, State2, 201);
{{see_other, ResURL}, Req2, State2} when Method =:= <<"POST">> ->
Req3 = cowboy_req:set_resp_header(
<<"location">>, ResURL, Req2),
respond(Req3, State2, 303);
{{true, ResURL}, Req2, State2} when Method =:= <<"POST">> -> {{true, ResURL}, Req2, State2} when Method =:= <<"POST">> ->
Req3 = cowboy_req:set_resp_header( Req3 = cowboy_req:set_resp_header(
<<"location">>, ResURL, Req2), <<"location">>, ResURL, Req2),

View file

@ -0,0 +1,28 @@
-module(create_resource_h).
-export([init/2]).
-export([allowed_methods/2]).
-export([resource_exists/2]).
-export([content_types_accepted/2]).
-export([from_text/2]).
init(Req, Opts) ->
{cowboy_rest, Req, Opts}.
allowed_methods(Req, State) ->
{[<<"POST">>], Req, State}.
resource_exists(Req, State) ->
{true, Req, State}.
content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"text">>, []}, from_text}], Req, State}.
from_text(Req=#{qs := Qs}, State) ->
NewURI = [cowboy_req:uri(Req), "/foo"],
case Qs of
<<"created">> ->
{{created, NewURI}, Req, State};
<<"see_other">> ->
{{see_other, NewURI}, Req, State}
end.

View file

@ -52,6 +52,7 @@ init_dispatch(_) ->
{"/content_types_accepted", content_types_accepted_h, []}, {"/content_types_accepted", content_types_accepted_h, []},
{"/content_types_provided", content_types_provided_h, []}, {"/content_types_provided", content_types_provided_h, []},
{"/delete_resource", delete_resource_h, []}, {"/delete_resource", delete_resource_h, []},
{"/create_resource", create_resource_h, []},
{"/expires", expires_h, []}, {"/expires", expires_h, []},
{"/generate_etag", generate_etag_h, []}, {"/generate_etag", generate_etag_h, []},
{"/if_range", if_range_h, []}, {"/if_range", if_range_h, []},
@ -474,6 +475,29 @@ delete_resource_missing(Config) ->
{response, _, 500, _} = gun:await(ConnPid, Ref), {response, _, 500, _} = gun:await(ConnPid, Ref),
ok. ok.
create_resource_created(Config) ->
doc("POST to an existing resource to create a new resource. "
"When the accept callback returns {created, NewURI}, "
"the expected reply is 201 Created."),
ConnPid = gun_open(Config),
Ref = gun:post(ConnPid, "/create_resource?created", [
{<<"content-type">>, <<"application/text">>}
], <<"hello">>, #{}),
{response, _, 201, _} = gun:await(ConnPid, Ref),
ok.
create_resource_see_other(Config) ->
doc("POST to an existing resource to create a new resource. "
"When the accept callback returns {see_other, NewURI}, "
"the expected reply is 303 See Other with a location header set."),
ConnPid = gun_open(Config),
Ref = gun:post(ConnPid, "/create_resource?see_other", [
{<<"content-type">>, <<"application/text">>}
], <<"hello">>, #{}),
{response, _, 303, RespHeaders} = gun:await(ConnPid, Ref),
{_, _} = lists:keyfind(<<"location">>, 1, RespHeaders),
ok.
error_on_malformed_accept(Config) -> error_on_malformed_accept(Config) ->
doc("A malformed Accept header must result in a 400 response."), doc("A malformed Accept header must result in a 400 response."),
do_error_on_malformed_header(Config, <<"accept">>). do_error_on_malformed_header(Config, <<"accept">>).