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

Add the chunked option for HTTP/1.1

It allows disabling the chunked transfer-encoding. It
can also be disabled on a per-request basis, although
it will be ignored for responses that are not streamed.
This commit is contained in:
Loïc Hoguin 2018-11-18 13:21:36 +01:00
parent 417032a445
commit 8d6d78575f
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
5 changed files with 129 additions and 14 deletions

View file

@ -24,23 +24,26 @@ experimental.
data in order to compress them. This is the case for data in order to compress them. This is the case for
gzip compression. gzip compression.
* Add an `http10_keepalive` option to allow disabling * Add the `chunked` option to allow disabling chunked
transfer-encoding for HTTP/1.1 connections.
* Add the `http10_keepalive` option to allow disabling
keep-alive for HTTP/1.0 connections. keep-alive for HTTP/1.0 connections.
* Add an `idle_timeout` option for HTTP/2. * Add the `idle_timeout` option for HTTP/2.
* Add a `sendfile` option to both HTTP/1.1 and HTTP/2. * Add the `sendfile` option to both HTTP/1.1 and HTTP/2.
It allows disabling the sendfile syscall entirely for It allows disabling the sendfile syscall entirely for
all connections. It is recommended to disable sendfile all connections. It is recommended to disable sendfile
when using VirtualBox shared folders. when using VirtualBox shared folders.
* Add the `rate_limited/2` callback to REST handlers. * Add the `rate_limited/2` callback to REST handlers.
* Add a `deflate_opts` option to Websocket handlers that * Add the `deflate_opts` option to Websocket handlers that
allows configuring deflate options for the allows configuring deflate options for the
permessage-deflate extension. permessage-deflate extension.
* Add a `charset` option to `cowboy_static`. * Add the `charset` option to `cowboy_static`.
* Add support for the SameSite cookie attribute. * Add support for the SameSite cookie attribute.
@ -81,8 +84,9 @@ experimental.
handlers and Websocket handlers. This can be used handlers and Websocket handlers. This can be used
to update options on a per-request basis. Allow to update options on a per-request basis. Allow
overriding the `idle_timeout` option for both overriding the `idle_timeout` option for both
HTTP/1.1 and Websocket, and the `cowboy_compress_h` HTTP/1.1 and Websocket, the `cowboy_compress_h`
options for HTTP/1.1 and HTTP/2. options for HTTP/1.1 and HTTP/2 and the `chunked`
option for HTTP/1.1.
=== Bugs fixed === Bugs fixed

View file

@ -17,6 +17,7 @@ as a Ranch protocol.
[source,erlang] [source,erlang]
---- ----
opts() :: #{ opts() :: #{
chunked => boolean(),
connection_type => worker | supervisor, connection_type => worker | supervisor,
env => cowboy_middleware:env(), env => cowboy_middleware:env(),
http10_keepalive => boolean(), http10_keepalive => boolean(),
@ -51,6 +52,13 @@ Ranch functions `ranch:get_protocol_options/1` and
The default value is given next to the option name: The default value is given next to the option name:
chunked (true)::
Whether chunked transfer-encoding is enabled for HTTP/1.1 connections.
Note that a response streamed to the client without the chunked
transfer-encoding and without a content-length header will result
in the connection being closed at the end of the response body.
connection_type (supervisor):: connection_type (supervisor)::
Whether the connection process also acts as a supervisor. Whether the connection process also acts as a supervisor.
@ -140,7 +148,7 @@ Ordered list of stream handlers that will handle all stream events.
== Changelog == Changelog
* *2.6*: The `http10_keepalive`, `proxy_header` and `sendfile` options were added. * *2.6*: The `chunked`, `http10_keepalive`, `proxy_header` and `sendfile` options were added.
* *2.5*: The `linger_timeout` option was added. * *2.5*: The `linger_timeout` option was added.
* *2.2*: The `max_skip_body_length` option was added. * *2.2*: The `max_skip_body_length` option was added.
* *2.0*: The `timeout` option was renamed `request_timeout`. * *2.0*: The `timeout` option was renamed `request_timeout`.

View file

@ -25,6 +25,7 @@
-export([system_code_change/4]). -export([system_code_change/4]).
-type opts() :: #{ -type opts() :: #{
chunked => boolean(),
compress_buffering => boolean(), compress_buffering => boolean(),
compress_threshold => non_neg_integer(), compress_threshold => non_neg_integer(),
connection_type => worker | supervisor, connection_type => worker | supervisor,
@ -963,21 +964,28 @@ commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, strea
end, end,
commands(State, StreamID, Tail); commands(State, StreamID, Tail);
%% Send response headers and initiate chunked encoding or streaming. %% Send response headers and initiate chunked encoding or streaming.
commands(State0=#state{socket=Socket, transport=Transport, streams=Streams0, out_state=OutState}, commands(State0=#state{socket=Socket, transport=Transport,
opts=Opts, overriden_opts=Override, streams=Streams0, out_state=OutState},
StreamID, [{headers, StatusCode, Headers0}|Tail]) -> StreamID, [{headers, StatusCode, Headers0}|Tail]) ->
%% @todo Same as above (about the last stream in the list). %% @todo Same as above (about the last stream in the list).
Stream = #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams0), Stream = #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams0),
Status = cow_http:status_to_integer(StatusCode), Status = cow_http:status_to_integer(StatusCode),
ContentLength = maps:get(<<"content-length">>, Headers0, undefined), ContentLength = maps:get(<<"content-length">>, Headers0, undefined),
%% Chunked transfer-encoding can be disabled on a per-request basis.
Chunked = case Override of
#{chunked := Chunked0} -> Chunked0;
_ -> maps:get(chunked, Opts, true)
end,
{State1, Headers1} = case {Status, ContentLength, Version} of {State1, Headers1} = case {Status, ContentLength, Version} of
{204, _, 'HTTP/1.1'} -> {204, _, 'HTTP/1.1'} ->
{State0#state{out_state=done}, Headers0}; {State0#state{out_state=done}, Headers0};
{304, _, 'HTTP/1.1'} -> {304, _, 'HTTP/1.1'} ->
{State0#state{out_state=done}, Headers0}; {State0#state{out_state=done}, Headers0};
{_, undefined, 'HTTP/1.1'} -> {_, undefined, 'HTTP/1.1'} when Chunked ->
{State0#state{out_state=chunked}, Headers0#{<<"transfer-encoding">> => <<"chunked">>}}; {State0#state{out_state=chunked}, Headers0#{<<"transfer-encoding">> => <<"chunked">>}};
%% Close the connection after streaming without content-length to HTTP/1.0 client. %% Close the connection after streaming without content-length
{_, undefined, 'HTTP/1.0'} -> %% to all HTTP/1.0 clients and to HTTP/1.1 clients when chunked is disabled.
{_, undefined, _} ->
{State0#state{out_state=streaming, last_streamid=StreamID}, Headers0}; {State0#state{out_state=streaming, last_streamid=StreamID}, Headers0};
%% Stream the response body without chunked transfer-encoding. %% Stream the response body without chunked transfer-encoding.
_ -> _ ->
@ -1099,12 +1107,18 @@ commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transpor
%% Set options dynamically. %% Set options dynamically.
commands(State0=#state{overriden_opts=Opts}, commands(State0=#state{overriden_opts=Opts},
StreamID, [{set_options, SetOpts}|Tail]) -> StreamID, [{set_options, SetOpts}|Tail]) ->
State = case SetOpts of State1 = case SetOpts of
#{idle_timeout := IdleTimeout} -> #{idle_timeout := IdleTimeout} ->
set_timeout(State0#state{overriden_opts=Opts#{idle_timeout => IdleTimeout}}); set_timeout(State0#state{overriden_opts=Opts#{idle_timeout => IdleTimeout}});
_ -> _ ->
State0 State0
end, end,
State = case SetOpts of
#{chunked := Chunked} ->
State1#state{overriden_opts=Opts#{chunked => Chunked}};
_ ->
State1
end,
commands(State, StreamID, Tail); commands(State, StreamID, Tail);
%% Stream shutdown. %% Stream shutdown.
commands(State, StreamID, [stop|Tail]) -> commands(State, StreamID, [stop|Tail]) ->

View file

@ -8,6 +8,19 @@
init(Req, State) -> init(Req, State) ->
set_options(cowboy_req:binding(key, Req), Req, State). set_options(cowboy_req:binding(key, Req), Req, State).
set_options(<<"chunked_false">>, Req0, State) ->
%% @todo This should be replaced by a cowboy_req:cast/cowboy_stream:cast.
#{pid := Pid, streamid := StreamID} = Req0,
Pid ! {{Pid, StreamID}, {set_options, #{chunked => false}}},
Req = cowboy_req:stream_reply(200, Req0),
cowboy_req:stream_body(<<0:8000000>>, fin, Req),
{ok, Req, State};
set_options(<<"chunked_false_ignored">>, Req0, State) ->
%% @todo This should be replaced by a cowboy_req:cast/cowboy_stream:cast.
#{pid := Pid, streamid := StreamID} = Req0,
Pid ! {{Pid, StreamID}, {set_options, #{chunked => false}}},
Req = cowboy_req:reply(200, #{}, <<"Hello world!">>, Req0),
{ok, Req, State};
set_options(<<"idle_timeout_short">>, Req0, State) -> set_options(<<"idle_timeout_short">>, Req0, State) ->
%% @todo This should be replaced by a cowboy_req:cast/cowboy_stream:cast. %% @todo This should be replaced by a cowboy_req:cast/cowboy_stream:cast.
#{pid := Pid, streamid := StreamID} = Req0, #{pid := Pid, streamid := StreamID} = Req0,

View file

@ -24,6 +24,8 @@
-import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_open/1]).
-import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_send/2]).
-import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv_head/1]).
-import(cowboy_test, [raw_recv/3]).
-import(cowboy_test, [raw_expect_recv/2]).
all() -> [{group, clear}]. all() -> [{group, clear}].
@ -33,12 +35,39 @@ init_routes(_) -> [
{"localhost", [ {"localhost", [
{"/", hello_h, []}, {"/", hello_h, []},
{"/echo/:key", echo_h, []}, {"/echo/:key", echo_h, []},
{"/resp/:key[/:arg]", resp_h, []},
{"/set_options/:key", set_options_h, []} {"/set_options/:key", set_options_h, []}
]} ]}
]. ].
chunked_false(Config) ->
doc("Confirm the option chunked => false disables chunked "
"transfer-encoding for HTTP/1.1 connections."),
{ok, _} = cowboy:start_clear(name(), [{port, 0}], #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))},
chunked => false
}),
Port = ranch:get_port(name()),
Request = "GET /resp/stream_reply2/200 HTTP/1.1\r\nhost: localhost\r\n\r\n",
Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]),
ok = raw_send(Client, Request),
Rest = case catch raw_recv_head(Client) of
{'EXIT', _} -> error(closed);
Data ->
%% Cowboy always advertises itself as HTTP/1.1.
{'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
{Headers, Rest1} = cow_http:parse_headers(Rest0),
false = lists:keyfind(<<"content-length">>, 1, Headers),
false = lists:keyfind(<<"transfer-encoding">>, 1, Headers),
Rest1
end,
Bits = 8000000 - bit_size(Rest),
raw_expect_recv(Client, <<0:Bits>>),
{error, closed} = raw_recv(Client, 1, 1000),
ok.
http10_keepalive_false(Config) -> http10_keepalive_false(Config) ->
doc("Confirm the option {http10_keepalive, false} disables keep-alive " doc("Confirm the option http10_keepalive => false disables keep-alive "
"completely for HTTP/1.0 connections."), "completely for HTTP/1.0 connections."),
{ok, _} = cowboy:start_clear(name(), [{port, 0}], #{ {ok, _} = cowboy:start_clear(name(), [{port, 0}], #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))}, env => #{dispatch => cowboy_router:compile(init_routes(Config))},
@ -101,6 +130,53 @@ request_timeout_infinity(Config) ->
ok ok
end. end.
set_options_chunked_false(Config) ->
doc("Confirm the option chunked can be dynamically set to disable "
"chunked transfer-encoding. This results in the closing of the "
"connection after the current request."),
{ok, _} = cowboy:start_clear(name(), [{port, 0}], #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))},
chunked => true
}),
Port = ranch:get_port(name()),
Request = "GET /set_options/chunked_false HTTP/1.1\r\nhost: localhost\r\n\r\n",
Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]),
ok = raw_send(Client, Request),
_ = case catch raw_recv_head(Client) of
{'EXIT', _} -> error(closed);
Data ->
%% Cowboy always advertises itself as HTTP/1.1.
{'HTTP/1.1', 200, _, Rest} = cow_http:parse_status_line(Data),
{Headers, <<>>} = cow_http:parse_headers(Rest),
false = lists:keyfind(<<"content-length">>, 1, Headers),
false = lists:keyfind(<<"transfer-encoding">>, 1, Headers)
end,
raw_expect_recv(Client, <<0:8000000>>),
{error, closed} = raw_recv(Client, 1, 1000),
ok.
set_options_chunked_false_ignored(Config) ->
doc("Confirm the option chunked can be dynamically set to disable "
"chunked transfer-encoding, and that it is ignored if the "
"response is not streamed."),
{ok, _} = cowboy:start_clear(name(), [{port, 0}], #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))},
chunked => true
}),
Port = ranch:get_port(name()),
ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
%% We do a first request setting the option but not
%% using chunked transfer-encoding in the response.
StreamRef1 = gun:get(ConnPid, "/set_options/chunked_false_ignored"),
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, StreamRef1),
%% We then do a second request to confirm that chunked
%% is not disabled for that second request.
StreamRef2 = gun:get(ConnPid, "/resp/stream_reply2/200"),
{response, nofin, 200, Headers} = gun:await(ConnPid, StreamRef2),
{_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, Headers),
ok.
set_options_idle_timeout(Config) -> set_options_idle_timeout(Config) ->
doc("Confirm that the idle_timeout option can be dynamically " doc("Confirm that the idle_timeout option can be dynamically "
"set to change how long Cowboy will wait before it closes the connection."), "set to change how long Cowboy will wait before it closes the connection."),