0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 20:30:23 +00:00

Add preliminary support for trailers in responses

This depends on changes in Cowlib that are only available on
master.
This commit is contained in:
Loïc Hoguin 2017-11-15 14:58:49 +01:00
parent a1ad482eb4
commit 39baed6c80
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
7 changed files with 120 additions and 7 deletions

View file

@ -16,7 +16,7 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl
LOCAL_DEPS = crypto LOCAL_DEPS = crypto
DEPS = cowlib ranch DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib 2.0.1 dep_cowlib = git https://github.com/ninenines/cowlib master
dep_ranch = git https://github.com/ninenines/ranch 1.4.0 dep_ranch = git https://github.com/ninenines/ranch 1.4.0
DOC_DEPS = asciideck DOC_DEPS = asciideck

View file

@ -69,6 +69,8 @@
state = undefined :: {module(), any()}, state = undefined :: {module(), any()},
%% Client HTTP version for this stream. %% Client HTTP version for this stream.
version = undefined :: cowboy:http_version(), version = undefined :: cowboy:http_version(),
%% Unparsed te header. Used to know if we can send trailers.
te :: undefined | binary(),
%% Commands queued. %% Commands queued.
queue = [] :: cowboy_stream:commands() queue = [] :: cowboy_stream:commands()
}). }).
@ -267,7 +269,9 @@ after_parse({request, Req=#{streamid := StreamID, headers := Headers, version :=
State0=#state{opts=Opts, streams=Streams0}, Buffer}) -> State0=#state{opts=Opts, streams=Streams0}, Buffer}) ->
try cowboy_stream:init(StreamID, Req, Opts) of try cowboy_stream:init(StreamID, Req, Opts) of
{Commands, StreamState} -> {Commands, StreamState} ->
Streams = [#stream{id=StreamID, state=StreamState, version=Version}|Streams0], TE = maps:get(<<"te">>, Headers, undefined),
Streams = [#stream{id=StreamID, state=StreamState,
version=Version, te=TE}|Streams0],
State1 = case maybe_req_close(State0, Headers, Version) of State1 = case maybe_req_close(State0, Headers, Version) of
close -> State0#state{streams=Streams, last_streamid=StreamID}; close -> State0#state{streams=Streams, last_streamid=StreamID};
keepalive -> State0#state{streams=Streams} keepalive -> State0#state{streams=Streams}
@ -900,6 +904,37 @@ commands(State0=#state{socket=Socket, transport=Transport, streams=Streams}, Str
nofin -> State0 nofin -> State0
end, end,
commands(State, StreamID, Tail); commands(State, StreamID, Tail);
%% Send trailers.
commands(State=#state{socket=Socket, transport=Transport, streams=Streams}, StreamID,
[{trailers, Trailers}|Tail]) ->
TE = case lists:keyfind(StreamID, #stream.id, Streams) of
%% HTTP/1.0 doesn't support chunked transfer-encoding.
#stream{version='HTTP/1.0'} ->
not_chunked;
%% No TE header was sent.
#stream{te=undefined} ->
no_trailers;
#stream{te=TE0} ->
try cow_http_hd:parse_te(TE0) of
{TE1, _} -> TE1
catch _:_ ->
%% If we can't parse the TE header, assume we can't send trailers.
no_trailers
end
end,
case TE of
trailers ->
Transport:send(Socket, [
<<"0\r\n">>,
cow_http:headers(maps:to_list(Trailers)),
<<"\r\n">>
]);
no_trailers ->
Transport:send(Socket, <<"0\r\n\r\n">>);
not_chunked ->
ok
end,
commands(State#state{out_state=done}, StreamID, Tail);
%% Send a file. %% Send a file.
commands(State0=#state{socket=Socket, transport=Transport}, StreamID, commands(State0=#state{socket=Socket, transport=Transport}, StreamID,
[{sendfile, IsFin, Offset, Bytes, Path}|Tail]) -> [{sendfile, IsFin, Offset, Bytes, Path}|Tail]) ->

View file

@ -49,7 +49,9 @@
%% Whether we finished receiving data. %% Whether we finished receiving data.
remote = nofin :: cowboy_stream:fin(), remote = nofin :: cowboy_stream:fin(),
%% Remote flow control window (how much we accept to receive). %% Remote flow control window (how much we accept to receive).
remote_window :: integer() remote_window :: integer(),
%% Unparsed te header. Used to know if we can send trailers.
te :: undefined | binary()
}). }).
-type stream() :: #stream{}. -type stream() :: #stream{}.
@ -537,9 +539,24 @@ commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeSta
commands(State0, Stream0=#stream{local=nofin}, [{data, IsFin, Data}|Tail]) -> commands(State0, Stream0=#stream{local=nofin}, [{data, IsFin, Data}|Tail]) ->
{State, Stream} = send_data(State0, Stream0, IsFin, Data), {State, Stream} = send_data(State0, Stream0, IsFin, Data),
commands(State, Stream, Tail); commands(State, Stream, Tail);
%% @todo data when local!=nofin %% @todo data when local!=nofin
%% Send trailers.
commands(State0, Stream0=#stream{local=nofin, te=TE0}, [{trailers, Trailers}|Tail]) ->
%% We only accept TE headers containing exactly "trailers" (RFC7540 8.1.2.1).
TE = try cow_http_hd:parse_te(TE0) of
{trailers, []} -> trailers;
_ -> no_trailers
catch _:_ ->
%% If we can't parse the TE header, assume we can't send trailers.
no_trailers
end,
{State, Stream} = case TE of
trailers ->
send_data(State0, Stream0, fin, {trailers, Trailers});
no_trailers ->
send_data(State0, Stream0, fin, <<>>)
end,
commands(State, Stream, Tail);
%% Send a file. %% Send a file.
commands(State0, Stream0=#stream{local=nofin}, commands(State0, Stream0=#stream{local=nofin},
[{sendfile, IsFin, Offset, Bytes, Path}|Tail]) -> [{sendfile, IsFin, Offset, Bytes, Path}|Tail]) ->
@ -664,6 +681,12 @@ send_data(State0, Stream0=#stream{local_buffer=Q0, local_buffer_size=BufferSize}
send_data(State, Stream, IsFin, Data) -> send_data(State, Stream, IsFin, Data) ->
send_data(State, Stream, IsFin, Data, in). send_data(State, Stream, IsFin, Data, in).
%% Always send trailer frames even if the window is empty.
send_data(State=#state{socket=Socket, transport=Transport, encode_state=EncodeState0},
Stream=#stream{id=StreamID}, fin, {trailers, Trailers}, _) ->
{HeaderBlock, EncodeState} = headers_encode(Trailers, EncodeState0),
Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)),
{State#state{encode_state=EncodeState}, Stream#stream{local=fin}};
%% Send data immediately if we can, buffer otherwise. %% Send data immediately if we can, buffer otherwise.
%% @todo We might want to print an error if local=fin. %% @todo We might want to print an error if local=fin.
send_data(State=#state{local_window=ConnWindow}, send_data(State=#state{local_window=ConnWindow},
@ -800,13 +823,14 @@ stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
stream_handler_init(State=#state{opts=Opts, stream_handler_init(State=#state{opts=Opts,
local_settings=#{initial_window_size := RemoteWindow}, local_settings=#{initial_window_size := RemoteWindow},
remote_settings=#{initial_window_size := LocalWindow}}, remote_settings=#{initial_window_size := LocalWindow}},
StreamID, RemoteIsFin, LocalIsFin, Req) -> StreamID, RemoteIsFin, LocalIsFin, Req=#{headers := Headers}) ->
try cowboy_stream:init(StreamID, Req, Opts) of try cowboy_stream:init(StreamID, Req, Opts) of
{Commands, StreamState} -> {Commands, StreamState} ->
commands(State#state{client_streamid=StreamID}, commands(State#state{client_streamid=StreamID},
#stream{id=StreamID, state=StreamState, #stream{id=StreamID, state=StreamState,
remote=RemoteIsFin, local=LocalIsFin, remote=RemoteIsFin, local=LocalIsFin,
local_window=LocalWindow, remote_window=RemoteWindow}, local_window=LocalWindow, remote_window=RemoteWindow,
te=maps:get(<<"te">>, Headers, undefined)},
Commands) Commands)
catch Class:Exception -> catch Class:Exception ->
cowboy_stream:report_error(init, cowboy_stream:report_error(init,

View file

@ -81,6 +81,7 @@
%% @todo stream_body/2 (nofin) %% @todo stream_body/2 (nofin)
-export([stream_body/3]). -export([stream_body/3]).
%% @todo stream_event/2,3 %% @todo stream_event/2,3
-export([stream_trailers/2]).
-export([push/3]). -export([push/3]).
-export([push/4]). -export([push/4]).
@ -774,6 +775,11 @@ stream_body(Data, IsFin, #{pid := Pid, streamid := StreamID, has_sent_resp := he
Pid ! {{Pid, StreamID}, {data, IsFin, Data}}, Pid ! {{Pid, StreamID}, {data, IsFin, Data}},
ok. ok.
-spec stream_trailers(cowboy:http_headers(), req()) -> ok.
stream_trailers(Trailers, #{pid := Pid, streamid := StreamID, has_sent_resp := headers}) ->
Pid ! {{Pid, StreamID}, {trailers, Trailers}},
ok.
-spec push(binary(), cowboy:http_headers(), req()) -> ok. -spec push(binary(), cowboy:http_headers(), req()) -> ok.
push(Path, Headers, Req) -> push(Path, Headers, Req) ->
push(Path, Headers, Req, #{}). push(Path, Headers, Req, #{}).

View file

@ -166,6 +166,8 @@ info(_StreamID, Headers = {headers, _, _}, State) ->
{[Headers], State#state{expect=undefined}}; {[Headers], State#state{expect=undefined}};
info(_StreamID, Data = {data, _, _}, State) -> info(_StreamID, Data = {data, _, _}, State) ->
{[Data], State}; {[Data], State};
info(_StreamID, Trailers = {trailers, _}, State) ->
{[Trailers], State};
info(_StreamID, Push = {push, _, _, _, _, _, _, _}, State) -> info(_StreamID, Push = {push, _, _, _, _, _, _, _}, State) ->
{[Push], State}; {[Push], State};
info(_StreamID, SwitchProtocol = {switch_protocol, _, _, _}, State) -> info(_StreamID, SwitchProtocol = {switch_protocol, _, _, _}, State) ->

View file

@ -204,6 +204,18 @@ do(<<"stream_body">>, Req0, Opts) ->
cowboy_req:stream_body(<<0:800000>>, fin, Req0), cowboy_req:stream_body(<<0:800000>>, fin, Req0),
{ok, Req0, Opts} {ok, Req0, Opts}
end; end;
do(<<"stream_trailers">>, Req0, Opts) ->
case cowboy_req:binding(arg, Req0) of
_ ->
Req = cowboy_req:stream_reply(200, #{
<<"trailer">> => <<"grpc-status">>
}, Req0),
cowboy_req:stream_body(<<"Hello world!">>, nofin, Req),
cowboy_req:stream_trailers(#{
<<"grpc-status">> => <<"0">>
}, Req),
{ok, Req, Opts}
end;
do(<<"push">>, Req, Opts) -> do(<<"push">>, Req, Opts) ->
case cowboy_req:binding(arg, Req) of case cowboy_req:binding(arg, Req) of
<<"method">> -> <<"method">> ->

View file

@ -841,6 +841,40 @@ stream_body_nofin(Config) ->
%% @todo Crash when calling stream_body after calling reply. %% @todo Crash when calling stream_body after calling reply.
%% @todo Crash when calling stream_body before calling stream_reply. %% @todo Crash when calling stream_body before calling stream_reply.
stream_trailers(Config) ->
doc("Stream body followed by trailer headers."),
{200, RespHeaders, <<"Hello world!">>, [
{<<"grpc-status">>, <<"0">>}
]} = do_trailers("/resp/stream_trailers", Config),
{_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders),
ok.
stream_trailers_no_te(Config) ->
doc("Stream body followed by trailer headers."),
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/resp/stream_trailers", [
{<<"accept-encoding">>, <<"gzip">>}
]),
{response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
{ok, RespBody} = gun:await_body(ConnPid, Ref),
gun:close(ConnPid).
do_trailers(Path, Config) ->
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, Path, [
{<<"accept-encoding">>, <<"gzip">>},
{<<"te">>, <<"trailers">>}
]),
{response, nofin, Status, RespHeaders} = gun:await(ConnPid, Ref),
{ok, RespBody, Trailers} = gun:await_body(ConnPid, Ref),
gun:close(ConnPid),
{Status, RespHeaders, do_decode(RespHeaders, RespBody), Trailers}.
%% @todo Crash when calling stream_trailers twice.
%% @todo Crash when calling stream_trailers after the fin flag has been set.
%% @todo Crash when calling stream_trailers after calling reply.
%% @todo Crash when calling stream_trailers before calling stream_reply.
%% Tests: Push. %% Tests: Push.
%% @todo We want to crash when push is called after reply has been initiated. %% @todo We want to crash when push is called after reply has been initiated.