mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Initial HTTP/3 implementation
This includes Websocket over HTTP/3. Since quicer, which provides the QUIC implementation, is a NIF, Cowboy cannot depend directly on it. In order to enable QUIC and HTTP/3, users have to set the COWBOY_QUICER environment variable: export COWBOY_QUICER=1 In order to run the test suites, the same must be done for Gun: export GUN_QUICER=1 HTTP/3 support is currently not available on Windows due to compilation issues of quicer which have yet to be looked at or resolved. HTTP/3 support is also unavailable on the upcoming OTP-27 due to compilation errors in quicer dependencies. Once resolved HTTP/3 should work on OTP-27. Because of how QUIC currently works, it's possible that streams that get reset after sending a response do not receive that response. The test suite was modified to accomodate for that. A future extension to QUIC will allow us to gracefully reset streams. This also updates Erlang.mk.
This commit is contained in:
parent
3ea8395eb8
commit
8cb9d242b0
39 changed files with 5130 additions and 238 deletions
|
@ -31,50 +31,42 @@ groups() ->
|
|||
|
||||
%% We set this module as a logger in order to silence expected errors.
|
||||
init_per_group(Name = http, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = https, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2c, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config),
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2_compress, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_plain_opts() ->
|
||||
#{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}.
|
||||
|
||||
init_compress_opts() ->
|
||||
#{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}.
|
||||
|
||||
%% Logger function silencing the expected crashes.
|
||||
|
||||
|
@ -99,15 +91,20 @@ crash_in_init(Config) ->
|
|||
%% Confirm terminate/3 is NOT called. We have no state to give to it.
|
||||
receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end,
|
||||
%% Confirm early_error/5 is called in HTTP/1.1's case.
|
||||
%% HTTP/2 does not send a response back so there is no early_error call.
|
||||
%% HTTP/2 and HTTP/3 do not send a response back so there is no early_error call.
|
||||
case config(protocol, Config) of
|
||||
http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end;
|
||||
http2 -> ok
|
||||
http2 -> ok;
|
||||
http3 -> ok
|
||||
end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
do_await_internal_error(ConnPid, Ref, Config) ->
|
||||
Protocol = config(protocol, Config),
|
||||
case {Protocol, gun:await(ConnPid, Ref)} of
|
||||
{http, {response, fin, 500, _}} -> ok;
|
||||
{http2, {error, {stream_error, {stream_error, internal_error, _}}}} -> ok;
|
||||
{http3, {error, {stream_error, {stream_error, h3_internal_error, _}}}} -> ok
|
||||
end.
|
||||
|
||||
crash_in_data(Config) ->
|
||||
|
@ -126,11 +123,7 @@ crash_in_data(Config) ->
|
|||
gun:data(ConnPid, Ref, fin, <<"Hello!">>),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_info(Config) ->
|
||||
doc("Confirm an error is sent when a stream handler crashes in info/3."),
|
||||
|
@ -144,14 +137,14 @@ crash_in_info(Config) ->
|
|||
%% Confirm init/3 is called.
|
||||
Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end,
|
||||
%% Send a message to make the stream handler crash.
|
||||
Pid ! {{Pid, 1}, crash},
|
||||
StreamID = case config(protocol, Config) of
|
||||
http3 -> 0;
|
||||
_ -> 1
|
||||
end,
|
||||
Pid ! {{Pid, StreamID}, crash},
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_terminate(Config) ->
|
||||
doc("Confirm the state is correct when a stream handler crashes in terminate/3."),
|
||||
|
@ -185,10 +178,12 @@ crash_in_terminate(Config) ->
|
|||
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error(Config) ->
|
||||
|
@ -225,10 +220,12 @@ do_crash_in_early_error(Config) ->
|
|||
{response, fin, 500, _} = gun:await(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error_fatal(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error_fatal(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error_fatal(Config) ->
|
||||
|
@ -262,7 +259,8 @@ early_error_stream_error_reason(Config) ->
|
|||
%% reason in both protocols.
|
||||
{Method, Headers, Status, Error} = case config(protocol, Config) of
|
||||
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error}
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error};
|
||||
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
|
||||
end,
|
||||
Ref = gun:request(ConnPid, Method, "/long_polling", [
|
||||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
|
@ -355,11 +353,20 @@ shutdown_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm we receive a DOWN message for the child process.
|
||||
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm we receive a DOWN message for the child process.
|
||||
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
shutdown_timeout_on_stream_stop(Config) ->
|
||||
doc("Confirm supervised processes are killed "
|
||||
|
@ -406,33 +413,45 @@ shutdown_timeout_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% We should NOT receive a DOWN message immediately.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
|
||||
%% We should receive it now.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% We should NOT receive a DOWN message immediately.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
|
||||
%% We should receive it now.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_timeout_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers_data(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers_data">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_response(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_response">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_switch_protocol_after_response(TestCase, Config) ->
|
||||
|
@ -502,7 +521,12 @@ terminate_on_stop(Config) ->
|
|||
{response, fin, 204, _} = gun:await(ConnPid, Ref),
|
||||
%% Confirm the stream is still alive even though we
|
||||
%% received the response fully, and tell it to stop.
|
||||
Pid ! {{Pid, 1}, please_stop},
|
||||
StreamID = case config(protocol, Config) of
|
||||
http -> 1;
|
||||
http2 -> 1;
|
||||
http3 -> 0
|
||||
end,
|
||||
Pid ! {{Pid, StreamID}, please_stop},
|
||||
receive {Self, Pid, info, _, please_stop, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm terminate/3 is called.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
|
@ -511,7 +535,8 @@ terminate_on_stop(Config) ->
|
|||
terminate_on_switch_protocol(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_terminate_on_switch_protocol(Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_terminate_on_switch_protocol(Config) ->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue