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

+ 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);
 			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),

+ 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_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">>).