Browse Source

AcceptCallback may now return created/see_other tuples for POST

They replace and deprecate the {true,URI} return value.
Martin Björklund 4 years ago
parent
commit
8795233c57

+ 13 - 4
doc/src/manual/cowboy_rest.asciidoc

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

+ 8 - 0
src/cowboy_rest.erl

@@ -1104,6 +1104,14 @@ process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) ->
 			next(Req2, State2, fun maybe_created/2);
 		{false, Req2, State2} ->
 			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">> ->
 			Req3 = cowboy_req:set_resp_header(
 				<<"location">>, ResURL, Req2),

+ 28 - 0
test/handlers/create_resource_h.erl

@@ -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.

+ 24 - 0
test/rest_handler_SUITE.erl

@@ -52,6 +52,7 @@ init_dispatch(_) ->
 		{"/content_types_accepted", content_types_accepted_h, []},
 		{"/content_types_provided", content_types_provided_h, []},
 		{"/delete_resource", delete_resource_h, []},
+		{"/create_resource", create_resource_h, []},
 		{"/expires", expires_h, []},
 		{"/generate_etag", generate_etag_h, []},
 		{"/if_range", if_range_h, []},
@@ -474,6 +475,29 @@ delete_resource_missing(Config) ->
 	{response, _, 500, _} = gun:await(ConnPid, Ref),
 	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) ->
 	doc("A malformed Accept header must result in a 400 response."),
 	do_error_on_malformed_header(Config, <<"accept">>).