aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/guide/resp.asciidoc23
-rw-r--r--doc/src/manual/cowboy_req.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.inform.asciidoc83
-rw-r--r--doc/src/manual/cowboy_req.push.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.reply.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.stream_reply.asciidoc1
-rw-r--r--src/cowboy_http2.erl7
-rw-r--r--src/cowboy_req.erl16
-rw-r--r--src/cowboy_stream_h.erl2
-rw-r--r--test/handlers/resp_h.erl31
-rw-r--r--test/req_SUITE.erl41
11 files changed, 206 insertions, 1 deletions
diff --git a/doc/src/guide/resp.asciidoc b/doc/src/guide/resp.asciidoc
index 2eaa804..6d4967e 100644
--- a/doc/src/guide/resp.asciidoc
+++ b/doc/src/guide/resp.asciidoc
@@ -262,6 +262,29 @@ Req = cowboy_req:reply(200, #{
// example would be automatic concatenation of CSS or JS
// 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
The HTTP/2 protocol introduced the ability to push resources
diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc
index b038764..b2875bc 100644
--- a/doc/src/manual/cowboy_req.asciidoc
+++ b/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: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: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: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
diff --git a/doc/src/manual/cowboy_req.inform.asciidoc b/doc/src/manual/cowboy_req.inform.asciidoc
new file mode 100644
index 0000000..d4421ba
--- /dev/null
+++ b/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)]
diff --git a/doc/src/manual/cowboy_req.push.asciidoc b/doc/src/manual/cowboy_req.push.asciidoc
index 200561f..5a3509f 100644
--- a/doc/src/manual/cowboy_req.push.asciidoc
+++ b/doc/src/manual/cowboy_req.push.asciidoc
@@ -94,5 +94,6 @@ cowboy_req:push("/static/style.css", #{
== See also
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:stream_reply(3)[cowboy_req:stream_reply(3)]
diff --git a/doc/src/manual/cowboy_req.reply.asciidoc b/doc/src/manual/cowboy_req.reply.asciidoc
index 37e32c9..7da306b 100644
--- a/doc/src/manual/cowboy_req.reply.asciidoc
+++ b/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_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:inform(3)[cowboy_req:inform(3)],
link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)],
link:man:cowboy_req:push(3)[cowboy_req:push(3)]
diff --git a/doc/src/manual/cowboy_req.stream_reply.asciidoc b/doc/src/manual/cowboy_req.stream_reply.asciidoc
index 8c4a7ae..19d46ca 100644
--- a/doc/src/manual/cowboy_req.stream_reply.asciidoc
+++ b/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_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:inform(3)[cowboy_req:inform(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:push(3)[cowboy_req:push(3)]
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index d863d1a..663d33a 100644
--- a/src/cowboy_http2.erl
+++ b/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, [{error_response, _, _, _}|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.
%%
%% @todo Kill the stream if it sent a response when one has already been sent.
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 1615c07..253564d 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -71,6 +71,8 @@
-export([set_resp_body/2]).
%% @todo set_resp_body/3 with a ContentType or even Headers argument, to set content headers.
-export([has_resp_body/1]).
+-export([inform/2]).
+-export([inform/3]).
-export([reply/2]).
-export([reply/3]).
-export([reply/4]).
@@ -685,6 +687,18 @@ has_resp_body(_) ->
delete_resp_header(Name, Req=#{resp_headers := 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().
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)
-> Req when Req::req().
reply(_, _, _, #{has_sent_resp := _}) ->
- error(function_clause);
+ error(function_clause); %% @todo Better error message.
reply(Status, Headers, {sendfile, _, 0, _}, Req)
when is_integer(Status); is_binary(Status) ->
do_reply(Status, Headers#{
diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl
index 5c674dd..c631c87 100644
--- a/src/cowboy_stream_h.erl
+++ b/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) ->
{[], State};
%% Response.
+info(_StreamID, Inform = {inform, _, _}, State) ->
+ {[Inform], State};
info(_StreamID, Response = {response, _, _, _}, State) ->
{[Response], State};
info(_StreamID, Headers = {headers, _, _}, State) ->
diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl
index 9b94e3f..94c7f60 100644
--- a/test/handlers/resp_h.erl
+++ b/test/handlers/resp_h.erl
@@ -100,6 +100,37 @@ do(<<"delete_resp_header">>, Req0, Opts) ->
Req = cowboy_req:delete_resp_header(<<"content-type">>, Req1),
false = cowboy_req:has_resp_header(<<"content-type">>, Req),
{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) ->
Req = case cowboy_req:binding(arg, Req0) of
<<"binary">> ->
diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl
index 26ced62..2d1fe38 100644
--- a/test/req_SUITE.erl
+++ b/test/req_SUITE.erl
@@ -114,6 +114,30 @@ do_get_body(Path, Config) ->
do_get_body(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) ->
case lists:keyfind(<<"content-encoding">>, 1, Headers) of
{_, <<"gzip">>} -> zlib:gunzip(Body);
@@ -703,6 +727,23 @@ delete_resp_header(Config) ->
false = lists:keymember(<<"content-type">>, 1, Headers),
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) ->
doc("Response with default headers and no body."),
{200, _, _} = do_get("/resp/reply2/200", Config),