From f3d6b05b863fe177a34a8a6ba48c5f263ef8cf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sun, 29 Oct 2017 19:52:27 +0000 Subject: Add cowboy_req:inform/2,3 User code can now send as many 1xx responses as necessary. --- doc/src/guide/resp.asciidoc | 23 +++++++ doc/src/manual/cowboy_req.asciidoc | 1 + doc/src/manual/cowboy_req.inform.asciidoc | 83 +++++++++++++++++++++++++ doc/src/manual/cowboy_req.push.asciidoc | 1 + doc/src/manual/cowboy_req.reply.asciidoc | 1 + doc/src/manual/cowboy_req.stream_reply.asciidoc | 1 + src/cowboy_http2.erl | 7 +++ src/cowboy_req.erl | 16 ++++- src/cowboy_stream_h.erl | 2 + test/handlers/resp_h.erl | 31 +++++++++ test/req_SUITE.erl | 41 ++++++++++++ 11 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 doc/src/manual/cowboy_req.inform.asciidoc 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">> => <<"; rel=preload; as=style">>, + <<"link">> => <<"; 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">> => <<"; rel=preload; as=style">>, + <<"link">> => <<"; 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), -- cgit v1.2.3