mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add the max_concurrent_streams h2 option
This commit is contained in:
parent
8bd55941aa
commit
7373822b86
4 changed files with 104 additions and 46 deletions
|
@ -21,6 +21,7 @@ opts() :: #{
|
|||
enable_connect_protocol => boolean(),
|
||||
env => cowboy_middleware:env(),
|
||||
inactivity_timeout => timeout(),
|
||||
max_concurrent_streams => non_neg_integer() | infinity,
|
||||
max_decode_table_size => non_neg_integer(),
|
||||
max_encode_table_size => non_neg_integer(),
|
||||
middlewares => [module()],
|
||||
|
@ -55,6 +56,9 @@ env (#{})::
|
|||
inactivity_timeout (300000)::
|
||||
Time in ms with nothing received at all before Cowboy closes the connection.
|
||||
|
||||
max_concurrent_streams (infinity)::
|
||||
Maximum number of concurrent streams allowed on the connection.
|
||||
|
||||
max_decode_table_size (4096)::
|
||||
Maximum header table size used by the decoder. This is the value advertised
|
||||
to the client. The client can then choose a header table size equal or lower
|
||||
|
@ -79,7 +83,9 @@ stream_handlers ([cowboy_stream_h])::
|
|||
|
||||
== Changelog
|
||||
|
||||
* *2.4*: Add the options `max_decode_table_size` and `max_encode_table_size`.
|
||||
* *2.4*: Add the options `max_concurrent_streams`,
|
||||
`max_decode_table_size` and `max_encode_table_size`
|
||||
to configure HTTP/2 SETTINGS.
|
||||
* *2.4*: Add the experimental option `enable_connect_protocol`.
|
||||
* *2.0*: Protocol introduced.
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
enable_connect_protocol => boolean(),
|
||||
env => cowboy_middleware:env(),
|
||||
inactivity_timeout => timeout(),
|
||||
max_concurrent_streams => non_neg_integer() | infinity,
|
||||
max_decode_table_size => non_neg_integer(),
|
||||
max_encode_table_size => non_neg_integer(),
|
||||
middlewares => [module()],
|
||||
|
@ -206,11 +207,12 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings,
|
|||
settings_init(State, Opts) ->
|
||||
S0 = setting_from_opt(#{}, Opts, max_decode_table_size,
|
||||
header_table_size, 4096),
|
||||
%% @todo max_concurrent_streams + enforce it
|
||||
S1 = setting_from_opt(S0, Opts, max_concurrent_streams,
|
||||
max_concurrent_streams, infinity),
|
||||
%% @todo initial_window_size
|
||||
%% @todo max_frame_size
|
||||
%% @todo max_header_list_size
|
||||
Settings = setting_from_opt(S0, Opts, enable_connect_protocol,
|
||||
Settings = setting_from_opt(S1, Opts, enable_connect_protocol,
|
||||
enable_connect_protocol, false),
|
||||
State#state{next_settings=Settings}.
|
||||
|
||||
|
@ -868,13 +870,24 @@ terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason) ->
|
|||
stream_decode_init(State=#state{decode_state=DecodeState0}, StreamID, IsFin, HeaderBlock) ->
|
||||
try cow_hpack:decode(HeaderBlock, DecodeState0) of
|
||||
{Headers, DecodeState} ->
|
||||
stream_pseudo_headers_init(State#state{decode_state=DecodeState},
|
||||
stream_enforce_concurrency_limit(State#state{decode_state=DecodeState},
|
||||
StreamID, IsFin, Headers)
|
||||
catch _:_ ->
|
||||
terminate(State, {connection_error, compression_error,
|
||||
'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'})
|
||||
end.
|
||||
|
||||
stream_enforce_concurrency_limit(State=#state{opts=Opts, streams=Streams},
|
||||
StreamID, IsFin, Headers) ->
|
||||
MaxConcurrentStreams = maps:get(max_concurrent_streams, Opts, infinity),
|
||||
case length(Streams) < MaxConcurrentStreams of
|
||||
true ->
|
||||
stream_pseudo_headers_init(State, StreamID, IsFin, Headers);
|
||||
false ->
|
||||
stream_refused(State, StreamID,
|
||||
'Maximum number of concurrent streams has been reached. (RFC7540 5.1.2)')
|
||||
end.
|
||||
|
||||
stream_pseudo_headers_init(State=#state{local_settings=LocalSettings},
|
||||
StreamID, IsFin, Headers0) ->
|
||||
IsExtendedConnectEnabled = maps:get(enable_connect_protocol, LocalSettings, false),
|
||||
|
@ -1045,6 +1058,10 @@ stream_malformed(State=#state{socket=Socket, transport=Transport}, StreamID, _)
|
|||
Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
|
||||
State.
|
||||
|
||||
stream_refused(State=#state{socket=Socket, transport=Transport}, StreamID, _) ->
|
||||
Transport:send(Socket, cow_http2:rst_stream(StreamID, refused_stream)),
|
||||
State.
|
||||
|
||||
stream_early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer,
|
||||
local_settings=#{initial_window_size := RemoteWindow},
|
||||
remote_settings=#{initial_window_size := LocalWindow},
|
||||
|
|
|
@ -30,7 +30,8 @@ init_per_suite(Config) ->
|
|||
skip;
|
||||
_ ->
|
||||
cowboy_test:init_http2(h2spec, #{
|
||||
env => #{dispatch => init_dispatch()}
|
||||
env => #{dispatch => init_dispatch()},
|
||||
max_concurrent_streams => 100
|
||||
}, Config)
|
||||
end.
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ init_routes(_) -> [
|
|||
{"localhost", [
|
||||
{"/", hello_h, []},
|
||||
{"/echo/:key", echo_h, []},
|
||||
{"/long_polling", long_polling_h, []},
|
||||
{"/resp/:key[/:arg]", resp_h, []}
|
||||
]}
|
||||
].
|
||||
|
@ -2263,34 +2264,6 @@ reject_streamid_lower(Config) ->
|
|||
% frame so that the client is forced to open a new connection for new
|
||||
% streams.
|
||||
|
||||
%% @todo We need this option too. (RFC7540 5.1.2)
|
||||
% A peer can limit the number of concurrently active streams using the
|
||||
% SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within
|
||||
% a SETTINGS frame. The maximum concurrent streams setting is specific
|
||||
% to each endpoint and applies only to the peer that receives the
|
||||
% setting. That is, clients specify the maximum number of concurrent
|
||||
% streams the server can initiate, and servers specify the maximum
|
||||
% number of concurrent streams the client can initiate.
|
||||
%
|
||||
% Streams that are in the "open" state or in either of the "half-
|
||||
% closed" states count toward the maximum number of streams that an
|
||||
% endpoint is permitted to open. Streams in any of these three states
|
||||
% count toward the limit advertised in the
|
||||
% SETTINGS_MAX_CONCURRENT_STREAMS setting. Streams in either of the
|
||||
% "reserved" states do not count toward the stream limit.
|
||||
%
|
||||
% Endpoints MUST NOT exceed the limit set by their peer. An endpoint
|
||||
% that receives a HEADERS frame that causes its advertised concurrent
|
||||
% stream limit to be exceeded MUST treat this as a stream error
|
||||
% (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice
|
||||
% of error code determines whether the endpoint wishes to enable
|
||||
% automatic retry (see Section 8.1.4) for details).
|
||||
%
|
||||
% An endpoint that wishes to reduce the value of
|
||||
% SETTINGS_MAX_CONCURRENT_STREAMS to a value that is below the current
|
||||
% number of open streams can either close streams that exceed the new
|
||||
% value or allow streams to complete.
|
||||
|
||||
%% (RFC7540 5.2.1)
|
||||
% 3. Flow control is directional with overall control provided by the
|
||||
% receiver. A receiver MAY choose to set any window size that it
|
||||
|
@ -2539,21 +2512,72 @@ settings_header_table_size_server(Config0) ->
|
|||
% 0. An endpoint that has both set this parameter to 0 and had it
|
||||
% acknowledged MUST treat the receipt of a PUSH_PROMISE frame as a
|
||||
% connection error (Section 5.4.1) of type PROTOCOL_ERROR.
|
||||
%% @todo settings_disable_push
|
||||
|
||||
settings_max_concurrent_streams(Config0) ->
|
||||
doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be used to "
|
||||
"restrict the number of concurrent streams. (RFC7540 5.1.2, RFC7540 6.5.2)"),
|
||||
%% Create a new listener that allows only a single concurrent stream.
|
||||
Config = cowboy_test:init_http(name(), #{
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
|
||||
max_concurrent_streams => 1
|
||||
}, Config0),
|
||||
{ok, Socket} = do_handshake(Config),
|
||||
%% Send two HEADERS frames as two separate streams.
|
||||
Headers = [
|
||||
{<<":method">>, <<"GET">>},
|
||||
{<<":scheme">>, <<"http">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<":path">>, <<"/long_polling">>}
|
||||
],
|
||||
{ReqHeadersBlock1, EncodeState} = cow_hpack:encode(Headers),
|
||||
{ReqHeadersBlock2, _} = cow_hpack:encode(Headers, EncodeState),
|
||||
ok = gen_tcp:send(Socket, [
|
||||
cow_http2:headers(1, fin, ReqHeadersBlock1),
|
||||
cow_http2:headers(3, fin, ReqHeadersBlock2)
|
||||
]),
|
||||
%% Receive a REFUSED_STREAM stream error.
|
||||
{ok, << _:24, 3:8, _:8, 3:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000),
|
||||
ok.
|
||||
|
||||
settings_max_concurrent_streams_0(Config0) ->
|
||||
doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be set to "
|
||||
"0 to refuse all incoming streams. (RFC7540 5.1.2, RFC7540 6.5.2)"),
|
||||
%% Create a new listener that allows only a single concurrent stream.
|
||||
Config = cowboy_test:init_http(name(), #{
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
|
||||
max_concurrent_streams => 0
|
||||
}, Config0),
|
||||
{ok, Socket} = do_handshake(Config),
|
||||
%% Send a HEADERS frame.
|
||||
{HeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"GET">>},
|
||||
{<<":scheme">>, <<"http">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<":path">>, <<"/long_polling">>}
|
||||
]),
|
||||
ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
|
||||
%% Receive a REFUSED_STREAM stream error.
|
||||
{ok, << _:24, 3:8, _:8, 1:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000),
|
||||
ok.
|
||||
|
||||
%% @todo The client can limit the number of concurrent streams too. (RFC7540 5.1.2)
|
||||
%
|
||||
% SETTINGS_MAX_CONCURRENT_STREAMS (0x3): Indicates the maximum number
|
||||
% of concurrent streams that the sender will allow. This limit is
|
||||
% directional: it applies to the number of streams that the sender
|
||||
% permits the receiver to create. Initially, there is no limit to
|
||||
% this value. It is recommended that this value be no smaller than
|
||||
% 100, so as to not unnecessarily limit parallelism.
|
||||
%
|
||||
% A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be
|
||||
% treated as special by endpoints. A zero value does prevent the
|
||||
% creation of new streams; however, this can also happen for any
|
||||
% limit that is exhausted with active streams. Servers SHOULD only
|
||||
% set a zero value for short durations; if a server does not wish to
|
||||
% accept requests, closing the connection is more appropriate.
|
||||
% A peer can limit the number of concurrently active streams using the
|
||||
% SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within
|
||||
% a SETTINGS frame. The maximum concurrent streams setting is specific
|
||||
% to each endpoint and applies only to the peer that receives the
|
||||
% setting. That is, clients specify the maximum number of concurrent
|
||||
% streams the server can initiate, and servers specify the maximum
|
||||
% number of concurrent streams the client can initiate.
|
||||
%
|
||||
% Endpoints MUST NOT exceed the limit set by their peer. An endpoint
|
||||
% that receives a HEADERS frame that causes its advertised concurrent
|
||||
% stream limit to be exceeded MUST treat this as a stream error
|
||||
% (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice
|
||||
% of error code determines whether the endpoint wishes to enable
|
||||
% automatic retry (see Section 8.1.4) for details).
|
||||
|
||||
% SETTINGS_INITIAL_WINDOW_SIZE (0x4):
|
||||
% Values above the maximum flow-control window size of 2^31-1 MUST
|
||||
% be treated as a connection error (Section 5.4.1) of type
|
||||
|
@ -2566,6 +2590,16 @@ settings_header_table_size_server(Config0) ->
|
|||
% Values outside this range MUST be treated as a connection error
|
||||
% (Section 5.4.1) of type PROTOCOL_ERROR.
|
||||
%
|
||||
% SETTINGS_MAX_HEADER_LIST_SIZE (0x6): This advisory setting informs a
|
||||
% peer of the maximum size of header list that the sender is
|
||||
% prepared to accept, in octets. The value is based on the
|
||||
% uncompressed size of header fields, including the length of the
|
||||
% name and value in octets plus an overhead of 32 octets for each
|
||||
% header field.
|
||||
%
|
||||
% For any given request, a lower limit than what is advertised MAY
|
||||
% be enforced. The initial value of this setting is unlimited.
|
||||
%
|
||||
% An endpoint that receives a SETTINGS frame with any unknown or
|
||||
% unsupported identifier MUST ignore that setting. (6.5.2 and 6.5.3)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue