Browse Source

Add cowboy_req:inform/2,3

User code can now send as many 1xx responses as necessary.
Loïc Hoguin 7 years ago
parent
commit
f3d6b05b86

+ 23 - 0
doc/src/guide/resp.asciidoc

@@ -262,6 +262,29 @@ Req = cowboy_req:reply(200, #{
 // example would be automatic concatenation of CSS or JS
 // example would be automatic concatenation of CSS or JS
 // files.
 // files.
 
 
+=== Informational responses
+
+Cowboy allows you to send informational responses.
+
+Informational responses are responses that have a status
+code between 100 and 199. Any number can be sent before
+the proper response. Sending an informational response
+does not change the behavior of the proper response, and
+clients are expected to ignore any informational response
+they do not understand.
+
+The following snippet sends a 103 informational response
+with some headers that are expected to be in the final
+response.
+
+[source,erlang]
+----
+Req = cowboy_req:inform(103, #{
+    <<"link">> => <<"</style.css>; rel=preload; as=style">>,
+    <<"link">> => <<"</script.js>; rel=preload; as=script">>
+}, Req0).
+----
+
 === Push
 === Push
 
 
 The HTTP/2 protocol introduced the ability to push resources
 The HTTP/2 protocol introduced the ability to push resources

+ 1 - 0
doc/src/manual/cowboy_req.asciidoc

@@ -80,6 +80,7 @@ Response:
 * link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)] - Delete a response header
 * link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)] - Delete a response header
 * link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)] - Set the response body
 * link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)] - Set the response body
 * link:man:cowboy_req:has_resp_body(3)[cowboy_req:has_resp_body(3)] - Is there a response body?
 * link:man:cowboy_req:has_resp_body(3)[cowboy_req:has_resp_body(3)] - Is there a response body?
+* link:man:cowboy_req:inform(3)[cowboy_req:inform(3)] - Send an informational response
 * link:man:cowboy_req:reply(3)[cowboy_req:reply(3)] - Send the response
 * link:man:cowboy_req:reply(3)[cowboy_req:reply(3)] - Send the response
 * link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] - Send the response headers
 * link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] - Send the response headers
 * link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)] - Stream the response body
 * link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)] - Stream the response body

+ 83 - 0
doc/src/manual/cowboy_req.inform.asciidoc

@@ -0,0 +1,83 @@
+= cowboy_req:inform(3)
+
+== Name
+
+cowboy_req:inform - Send an informational response
+
+== Description
+
+[source,erlang]
+----
+inform(Status, Req :: cowboy_req:req())
+    -> inform(StatusCode, #{}, Req)
+
+inform(Status, Headers, Req :: cowboy_req:req())
+    -> ok
+
+Status  :: cowboy:http_status()
+Headers :: cowboy:http_headers()
+----
+
+Send an informational response.
+
+Informational responses use a status code between 100 and 199.
+They cannot include a body. This function will not use any
+of the previously set headers. All headers to be sent must
+be given directly.
+
+Any number of informational responses can be sent as long as
+they are sent before the proper response. Attempting to use
+this function after sending a normal response will result
+in an error.
+
+The header names must be given as lowercase binary strings.
+While header names are case insensitive, Cowboy requires them
+to be given as lowercase to function properly.
+
+== Arguments
+
+Status::
+
+The status code for the response.
+
+Headers::
+
+The response headers.
+
+Header names must be given as lowercase binary strings.
+
+Req::
+
+The Req object.
+
+== Return value
+
+The atom `ok` is always returned. It can be safely ignored.
+
+== Changelog
+
+* *2.0*: Function introduced.
+
+== Examples
+
+.Send an informational response
+[source,erlang]
+----
+Req = cowboy_req:inform(102, Req0).
+----
+
+.Send an informational response with headers
+[source,erlang]
+----
+Req = cowboy_req:inform(103, #{
+    <<"link">> => <<"</style.css>; rel=preload; as=style">>,
+    <<"link">> => <<"</script.js>; rel=preload; as=script">>
+}, Req0).
+----
+
+== See also
+
+link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:reply(3)[cowboy_req:reply(3)],
+link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)],
+link:man:cowboy_req:push(3)[cowboy_req:push(3)]

+ 1 - 0
doc/src/manual/cowboy_req.push.asciidoc

@@ -94,5 +94,6 @@ cowboy_req:push("/static/style.css", #{
 == See also
 == See also
 
 
 link:man:cowboy_req(3)[cowboy_req(3)],
 link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:inform(3)[cowboy_req:inform(3)],
 link:man:cowboy_req:reply(3)[cowboy_req:reply(3)],
 link:man:cowboy_req:reply(3)[cowboy_req:reply(3)],
 link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)]
 link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)]

+ 1 - 0
doc/src/manual/cowboy_req.reply.asciidoc

@@ -113,5 +113,6 @@ link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)],
 link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)],
 link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)],
 link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)],
 link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)],
 link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)],
 link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)],
+link:man:cowboy_req:inform(3)[cowboy_req:inform(3)],
 link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)],
 link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)],
 link:man:cowboy_req:push(3)[cowboy_req:push(3)]
 link:man:cowboy_req:push(3)[cowboy_req:push(3)]

+ 1 - 0
doc/src/manual/cowboy_req.stream_reply.asciidoc

@@ -103,6 +103,7 @@ link:man:cowboy_req(3)[cowboy_req(3)],
 link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)],
 link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)],
 link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)],
 link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)],
 link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)],
 link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)],
+link:man:cowboy_req:inform(3)[cowboy_req:inform(3)],
 link:man:cowboy_req:reply(3)[cowboy_req:reply(3)],
 link:man:cowboy_req:reply(3)[cowboy_req:reply(3)],
 link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)],
 link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)],
 link:man:cowboy_req:push(3)[cowboy_req:push(3)]
 link:man:cowboy_req:push(3)[cowboy_req:push(3)]

+ 7 - 0
src/cowboy_http2.erl

@@ -495,6 +495,13 @@ commands(State, Stream=#stream{local=idle}, [{error_response, StatusCode, Header
 	commands(State, Stream, [{response, StatusCode, Headers, Body}|Tail]);
 	commands(State, Stream, [{response, StatusCode, Headers, Body}|Tail]);
 commands(State, Stream, [{error_response, _, _, _}|Tail]) ->
 commands(State, Stream, [{error_response, _, _, _}|Tail]) ->
 	commands(State, Stream, Tail);
 	commands(State, Stream, Tail);
+%% Send an informational response.
+commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeState0},
+		Stream=#stream{id=StreamID, local=idle}, [{inform, StatusCode, Headers0}|Tail]) ->
+	Headers = Headers0#{<<":status">> => status(StatusCode)},
+	{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
+	Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)),
+	commands(State#state{encode_state=EncodeState}, Stream, Tail);
 %% Send response headers.
 %% Send response headers.
 %%
 %%
 %% @todo Kill the stream if it sent a response when one has already been sent.
 %% @todo Kill the stream if it sent a response when one has already been sent.

+ 15 - 1
src/cowboy_req.erl

@@ -71,6 +71,8 @@
 -export([set_resp_body/2]).
 -export([set_resp_body/2]).
 %% @todo set_resp_body/3 with a ContentType or even Headers argument, to set content headers.
 %% @todo set_resp_body/3 with a ContentType or even Headers argument, to set content headers.
 -export([has_resp_body/1]).
 -export([has_resp_body/1]).
+-export([inform/2]).
+-export([inform/3]).
 -export([reply/2]).
 -export([reply/2]).
 -export([reply/3]).
 -export([reply/3]).
 -export([reply/4]).
 -export([reply/4]).
@@ -685,6 +687,18 @@ has_resp_body(_) ->
 delete_resp_header(Name, Req=#{resp_headers := RespHeaders}) ->
 delete_resp_header(Name, Req=#{resp_headers := RespHeaders}) ->
 	Req#{resp_headers => maps:remove(Name, RespHeaders)}.
 	Req#{resp_headers => maps:remove(Name, RespHeaders)}.
 
 
+-spec inform(cowboy:http_status(), req()) -> ok.
+inform(Status, Req) ->
+	inform(Status, #{}, Req).
+
+-spec inform(cowboy:http_status(), cowboy:http_headers(), req()) -> ok.
+inform(_, _, #{has_sent_resp := _}) ->
+	error(function_clause); %% @todo Better error message.
+inform(Status, Headers, #{pid := Pid, streamid := StreamID})
+		when is_integer(Status); is_binary(Status) ->
+	Pid ! {{Pid, StreamID}, {inform, Status, Headers}},
+	ok.
+
 -spec reply(cowboy:http_status(), Req) -> Req when Req::req().
 -spec reply(cowboy:http_status(), Req) -> Req when Req::req().
 reply(Status, Req) ->
 reply(Status, Req) ->
 	reply(Status, #{}, Req).
 	reply(Status, #{}, Req).
@@ -699,7 +713,7 @@ reply(Status, Headers, Req) ->
 -spec reply(cowboy:http_status(), cowboy:http_headers(), resp_body(), Req)
 -spec reply(cowboy:http_status(), cowboy:http_headers(), resp_body(), Req)
 	-> Req when Req::req().
 	-> Req when Req::req().
 reply(_, _, _, #{has_sent_resp := _}) ->
 reply(_, _, _, #{has_sent_resp := _}) ->
-	error(function_clause);
+	error(function_clause); %% @todo Better error message.
 reply(Status, Headers, {sendfile, _, 0, _}, Req)
 reply(Status, Headers, {sendfile, _, 0, _}, Req)
 		when is_integer(Status); is_binary(Status) ->
 		when is_integer(Status); is_binary(Status) ->
 	do_reply(Status, Headers#{
 	do_reply(Status, Headers#{

+ 2 - 0
src/cowboy_stream_h.erl

@@ -119,6 +119,8 @@ info(_StreamID, {read_body_timeout, Ref}, State=#state{pid=Pid, read_body_ref=Re
 info(_StreamID, {read_body_timeout, _}, State) ->
 info(_StreamID, {read_body_timeout, _}, State) ->
 	{[], State};
 	{[], State};
 %% Response.
 %% Response.
+info(_StreamID, Inform = {inform, _, _}, State) ->
+	{[Inform], State};
 info(_StreamID, Response = {response, _, _, _}, State) ->
 info(_StreamID, Response = {response, _, _, _}, State) ->
 	{[Response], State};
 	{[Response], State};
 info(_StreamID, Headers = {headers, _, _}, State) ->
 info(_StreamID, Headers = {headers, _, _}, State) ->

+ 31 - 0
test/handlers/resp_h.erl

@@ -100,6 +100,37 @@ do(<<"delete_resp_header">>, Req0, Opts) ->
 	Req = cowboy_req:delete_resp_header(<<"content-type">>, Req1),
 	Req = cowboy_req:delete_resp_header(<<"content-type">>, Req1),
 	false = cowboy_req:has_resp_header(<<"content-type">>, Req),
 	false = cowboy_req:has_resp_header(<<"content-type">>, Req),
 	{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
 	{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
+do(<<"inform2">>, Req0, Opts) ->
+	case cowboy_req:binding(arg, Req0) of
+		<<"binary">> ->
+			cowboy_req:inform(<<"102 On my way">>, Req0);
+		<<"error">> ->
+			ct_helper:ignore(cowboy_req, inform, 3),
+			cowboy_req:inform(ok, Req0);
+		<<"twice">> ->
+			cowboy_req:inform(102, Req0),
+			cowboy_req:inform(102, Req0);
+		Status ->
+			cowboy_req:inform(binary_to_integer(Status), Req0)
+	end,
+	Req = cowboy_req:reply(200, Req0),
+	{ok, Req, Opts};
+do(<<"inform3">>, Req0, Opts) ->
+	Headers = #{<<"ext-header">> => <<"ext-value">>},
+	case cowboy_req:binding(arg, Req0) of
+		<<"binary">> ->
+			cowboy_req:inform(<<"102 On my way">>, Headers, Req0);
+		<<"error">> ->
+			ct_helper:ignore(cowboy_req, inform, 3),
+			cowboy_req:inform(ok, Headers, Req0);
+		<<"twice">> ->
+			cowboy_req:inform(102, Headers, Req0),
+			cowboy_req:inform(102, Headers, Req0);
+		Status ->
+			cowboy_req:inform(binary_to_integer(Status), Headers, Req0)
+	end,
+	Req = cowboy_req:reply(200, Req0),
+	{ok, Req, Opts};
 do(<<"reply2">>, Req0, Opts) ->
 do(<<"reply2">>, Req0, Opts) ->
 	Req = case cowboy_req:binding(arg, Req0) of
 	Req = case cowboy_req:binding(arg, Req0) of
 		<<"binary">> ->
 		<<"binary">> ->

+ 41 - 0
test/req_SUITE.erl

@@ -114,6 +114,30 @@ do_get_body(Path, Config) ->
 do_get_body(Path, Headers, Config) ->
 do_get_body(Path, Headers, Config) ->
 	do_body("GET", Path, Headers, Config).
 	do_body("GET", Path, Headers, Config).
 
 
+do_get_inform(Path, Config) ->
+	ConnPid = gun_open(Config),
+	Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]),
+	case gun:await(ConnPid, Ref) of
+		{response, _, RespStatus, RespHeaders} ->
+			%% We don't care about the body.
+			gun:close(ConnPid),
+			{RespStatus, RespHeaders};
+		{inform, InfoStatus, InfoHeaders} ->
+			{response, IsFin, RespStatus, RespHeaders}
+				= case gun:await(ConnPid, Ref) of
+					{inform, InfoStatus, InfoHeaders} ->
+						gun:await(ConnPid, Ref);
+					Response ->
+						Response
+			end,
+			{ok, RespBody} = case IsFin of
+				nofin -> gun:await_body(ConnPid, Ref);
+				fin -> {ok, <<>>}
+			end,
+			gun:close(ConnPid),
+			{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)}
+	end.
+
 do_decode(Headers, Body) ->
 do_decode(Headers, Body) ->
 	case lists:keyfind(<<"content-encoding">>, 1, Headers) of
 	case lists:keyfind(<<"content-encoding">>, 1, Headers) of
 		{_, <<"gzip">>} -> zlib:gunzip(Body);
 		{_, <<"gzip">>} -> zlib:gunzip(Body);
@@ -703,6 +727,23 @@ delete_resp_header(Config) ->
 	false = lists:keymember(<<"content-type">>, 1, Headers),
 	false = lists:keymember(<<"content-type">>, 1, Headers),
 	ok.
 	ok.
 
 
+inform2(Config) ->
+	doc("Informational response(s) without headers, followed by the real response."),
+	{102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config),
+	{102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
+	{500, _} = do_get_inform("/resp/inform2/error", Config),
+	{102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config),
+	ok.
+
+inform3(Config) ->
+	doc("Informational response(s) with headers, followed by the real response."),
+	Headers = [{<<"ext-header">>, <<"ext-value">>}],
+	{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
+	{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
+	{500, _} = do_get_inform("/resp/inform3/error", Config),
+	{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config),
+	ok.
+
 reply2(Config) ->
 reply2(Config) ->
 	doc("Response with default headers and no body."),
 	doc("Response with default headers and no body."),
 	{200, _, _} = do_get("/resp/reply2/200", Config),
 	{200, _, _} = do_get("/resp/reply2/200", Config),