Loïc Hoguin 1 year ago
parent
commit
00eb7a2330

+ 1 - 1
ebin/cowboy.app

@@ -6,4 +6,4 @@
 	{applications, [kernel,stdlib,crypto,cowlib,ranch,quicer]},
 	{mod, {cowboy_app, []}},
 	{env, []}
-]}.
+]}.

+ 7 - 4
src/cowboy.erl

@@ -77,10 +77,13 @@ start_quic(TransOpts, ProtoOpts) ->
 		{peer_unidi_stream_count, 3}, %% We only need control and QPACK enc/dec.
 		{peer_bidi_stream_count, 100}
 	|SocketOpts0],
-	{ok, Listen} = quicer:listen(Port, SocketOpts),
-	ListenerPid = spawn(fun AcceptLoop() ->
-		{ok, Conn} = quicer:accept(Listen, []),
+	{ok, Listener} = quicer:listen(Port, SocketOpts),
+	ct:pal("listen ~p", [Listener]),
+	_ListenerPid = spawn(fun AcceptLoop() ->
+		{ok, Conn} = quicer:accept(Listener, []),
+		ct:pal("accept ~p", [Conn]),
 		{ok, Conn} = quicer:handshake(Conn),
+		ct:pal("handshake ~p", [Conn]),
 		Pid = spawn(fun() ->
 			receive go -> ok end,
 			process_flag(trap_exit, true), %% @todo Only if supervisor though.
@@ -94,7 +97,7 @@ start_quic(TransOpts, ProtoOpts) ->
 		Pid ! go,
 		AcceptLoop()
 	end),
-	{ok, ListenerPid}.
+	{ok, Listener}.
 
 -spec start_quic_test() -> ok.
 start_quic_test() ->

+ 47 - 15
src/cowboy_http3.erl

@@ -37,7 +37,7 @@
 	buffer = <<>> :: binary(),
 
 	%% Stream state.
-	state :: {module, any()}
+	state = undefined :: undefined | {module, any()}
 }).
 
 -record(state, {
@@ -75,6 +75,7 @@
 
 -spec init(_, _, _) -> no_return().
 init(Parent, Conn, Opts) ->
+ct:pal("init"),
 	{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts),
 	%% Immediately open a control, encoder and decoder stream.
 	{ok, ControlRef} = quicer:start_stream(Conn,
@@ -112,7 +113,8 @@ init(Parent, Conn, Opts) ->
 				'A socket error occurred when retrieving the sock name.'})
 	end.
 
-loop(State0=#state{conn=Conn}) ->
+loop(State0=#state{conn=Conn, children=Children}) ->
+%ct:pal("~p", [process_info(self(), messages)]),
 	receive
 		%% Stream data.
 		%% @todo IsFin is inside Props. But it may not be set once the data was sent.
@@ -156,6 +158,10 @@ loop(State0=#state{conn=Conn}) ->
 		%% QUIC_STREAM_EVENT_SEND_SHUTDOWN_COMPLETE
 		{quic, send_shutdown_complete, _StreamRef, _IsGraceful} ->
 			loop(State0);
+		%% Timeouts.
+		{timeout, Ref, {shutdown, Pid}} ->
+			cowboy_children:shutdown_timeout(Children, Ref, Pid),
+			loop(State0);
 		%% Messages pertaining to a stream.
 		{{Pid, StreamID}, Msg} when Pid =:= self() ->
 			loop(info(State0, StreamID, Msg));
@@ -477,7 +483,7 @@ down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) ->
 	end.
 
 info(State=#state{opts=Opts, http3_machine=_HTTP3Machine}, StreamID, Msg) ->
-%ct:pal("INFO ~p", [Msg]),
+%ct:pal("INFO ~p ~p ~p", [State, StreamID, Msg]),
 	case stream_get(State, StreamID) of
 		Stream=#stream{state=StreamState0} ->
 			try cowboy_stream:info(StreamID, Msg, StreamState0) of
@@ -522,17 +528,17 @@ commands(State0, Stream, [{inform, StatusCode, Headers}|Tail]) ->
 	commands(State, Stream, Tail);
 %% Send response headers.
 commands(State0, Stream, [{response, StatusCode, Headers, Body}|Tail]) ->
-	ct:pal("commands response ~p ~p ~p", [StatusCode, Headers, try iolist_size(Body) catch _:_ -> Body end]),
+%	ct:pal("commands response ~p ~p ~p", [StatusCode, Headers, try iolist_size(Body) catch _:_ -> Body end]),
 	State = send_response(State0, Stream, StatusCode, Headers, Body),
 	commands(State, Stream, Tail);
 %% Send response headers.
 commands(State0, Stream, [{headers, StatusCode, Headers}|Tail]) ->
-	ct:pal("commands headers ~p ~p", [StatusCode, Headers]),
+%	ct:pal("commands headers ~p ~p", [StatusCode, Headers]),
 	State = send_headers(State0, Stream, nofin, StatusCode, Headers),
 	commands(State, Stream, Tail);
 %%% Send a response body chunk.
 commands(State0, Stream=#stream{ref=StreamRef}, [{data, IsFin, Data}|Tail]) ->
-	ct:pal("commands data ~p ~p", [IsFin, try iolist_size(Data) catch _:_ -> Data end]),
+%	ct:pal("commands data ~p ~p", [IsFin, try iolist_size(Data) catch _:_ -> Data end]),
 	_ = case Data of
 		{sendfile, Offset, Bytes, Path} ->
 			%% Temporary solution to do sendfile over QUIC.
@@ -548,7 +554,7 @@ commands(State0, Stream=#stream{ref=StreamRef}, [{data, IsFin, Data}|Tail]) ->
 commands(State=#state{http3_machine=HTTP3Machine0},
 		Stream=#stream{id=StreamID, ref=StreamRef},
 		[{trailers, Trailers}|Tail]) ->
-	ct:pal("commands trailers ~p", [Trailers]),
+%	ct:pal("commands trailers ~p", [Trailers]),
 	HTTP3Machine = case cow_http3_machine:prepare_trailers(
 			StreamID, HTTP3Machine0, maps:to_list(Trailers)) of
 		{trailers, HeaderBlock, _EncData, HTTP3Machine1} ->
@@ -700,7 +706,7 @@ send_headers(State=#state{http3_machine=HTTP3Machine0},
 		= cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, IsFin0,
 			#{status => cow_http:status_to_integer(StatusCode)},
 			headers_to_list(Headers)),
-	{ok, _} = quicer:send(StreamRef, cow_http3:headers(HeaderBlock), send_flag(IsFin)),
+	quicer:send(StreamRef, cow_http3:headers(HeaderBlock), send_flag(IsFin)),
 	%% @todo Send _EncData.
 	State#state{http3_machine=HTTP3Machine}.
 
@@ -716,6 +722,7 @@ send_flag(fin) -> ?QUIC_SEND_FLAG_FIN.
 
 reset_stream(State0=#state{http3_machine=HTTP3Machine0},
 		Stream=#stream{id=StreamID, ref=StreamRef}, Error) ->
+%ct:pal("~p ~p", [Stream, Error]),
 	Reason = case Error of
 		{internal_error, _, _} -> h3_internal_error;
 		{stream_error, Reason0, _} -> Reason0
@@ -741,12 +748,15 @@ reset_stream(State0=#state{http3_machine=HTTP3Machine0},
 	State1.
 
 stop_stream(State0=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamID}) ->
+%ct:pal("stop_stream ~p ~p", [State0, Stream]),
 	%% We abort reading when stopping the stream but only
 	%% if the client was not finished sending data.
 	%% We mark the stream as 'stopping' either way.
 	State = case cow_http3_machine:get_stream_remote_state(StreamID, HTTP3Machine) of
 		{ok, fin} ->
 			stream_store(State0, Stream#stream{status=stopping});
+		{error, not_found} ->
+			stream_store(State0, Stream#stream{status=stopping});
 		_ ->
 			stream_abort_receive(State0, Stream, h3_no_error)
 	end,
@@ -760,7 +770,7 @@ stop_stream(State0=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamI
 		%% When a response was sent but not terminated, we need to close the stream.
 		%% We send a final DATA frame to complete the stream.
 		{ok, nofin} ->
-			ct:pal("error nofin"),
+%			ct:pal("error nofin"),
 			info(State, StreamID, {data, fin, <<>>});
 		%% When a response was sent fully we can terminate the stream,
 		%% regardless of the stream being in half-closed or closed state.
@@ -860,14 +870,36 @@ stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
 %	ct:pal("new stream ~p ~p", [Stream, HTTP3Machine]),
 	State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}.
 
-stream_closed(State=#state{http3_machine=HTTP3Machine0, streams=Streams0},
-		StreamID, _Flags) ->
+stream_closed(State=#state{opts=Opts, http3_machine=HTTP3Machine0,
+		streams=Streams0, children=Children0}, StreamID, #{error := ErrorCode}) ->
 	case cow_http3_machine:close_stream(StreamID, HTTP3Machine0) of
 		{ok, HTTP3Machine} ->
-			%% @todo Some streams may not be bidi or remote.
-			Streams = maps:remove(StreamID, Streams0),
-			%% @todo terminate stream if necessary
-			State#state{http3_machine=HTTP3Machine, streams=Streams};
+			case maps:take(StreamID, Streams0) of
+				{#stream{state=undefined}, Streams} ->
+					%% Unidi stream has no handler/children.
+					State#state{http3_machine=HTTP3Machine, streams=Streams};
+				%% We only stop bidi streams if the stream was closed with an error
+				%% or the stream was already in the process of stopping.
+				{#stream{status=Status, state=StreamState}, Streams}
+						when Status =:= stopping; ErrorCode =/= 0 ->
+					terminate_stream_handler(State, StreamID, closed, StreamState),
+					Children = cowboy_children:shutdown(Children0, StreamID),
+					State#state{http3_machine=HTTP3Machine, streams=Streams, children=Children};
+				%% Don't remove a stream that terminated properly but
+				%% has chosen to remain up (custom stream handlers).
+				{_, _} ->
+					State#state{http3_machine=HTTP3Machine};
+				error ->
+					case is_lingering_stream(State, StreamID) of
+						true ->
+							ok;
+						false ->
+							%% We avoid logging the data as it could be quite large.
+							cowboy:log(warning, "Received stream_closed for unknown stream ~p.",
+								[StreamID], Opts)
+					end,
+					State
+			end;
 		{error, Error={connection_error, _, _}, HTTP3Machine} ->
 			terminate(State#state{http3_machine=HTTP3Machine}, Error)
 	end.

+ 3 - 3
test/compress_SUITE.erl

@@ -27,8 +27,8 @@ all() ->
 		{group, http_compress},
 		{group, https_compress},
 		{group, h2_compress},
-		{group, h2c_compress}
-		%% @todo h3_compress
+		{group, h2c_compress},
+		{group, h3_compress}
 	].
 
 groups() ->
@@ -38,7 +38,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Routes.
 

+ 24 - 7
test/cowboy_test.erl

@@ -48,13 +48,23 @@ init_http3(Ref, ProtoOpts, Config) ->
 		++ "/rfc9114_SUITE_data",
 	TransOpts = #{
 		socket_opts => [
-			{cert, DataDir ++ "/server.pem"},
-			{key, DataDir ++ "/server.key"}
+			{certfile, DataDir ++ "/server.pem"},
+			{keyfile, DataDir ++ "/server.key"}
 		]
 	},
-	{ok, _} = cowboy:start_quic(TransOpts, ProtoOpts), %% @todo Ref argument.
+	{ok, Listener} = cowboy:start_quic(TransOpts, ProtoOpts), %% @todo Ref argument.
+	%% @todo Keep listener information around in a better place.
+	persistent_term:put({cowboy_test_quic, Ref}, Listener),
 	[{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, TransOpts}|Config].
 
+stop_group(Ref) ->
+	case persistent_term:get({cowboy_test_quic, Ref}, undefined) of
+		undefined ->
+			cowboy:stop_listener(Ref);
+		Listener ->
+			quicer:close_listener(Listener)
+	end.
+
 %% Common group of listeners used by most suites.
 
 common_all() ->
@@ -67,7 +77,8 @@ common_all() ->
 		{group, http_compress},
 		{group, https_compress},
 		{group, h2_compress},
-		{group, h2c_compress}
+		{group, h2c_compress},
+		{group, h3_compress}
 	].
 
 common_groups(Tests) ->
@@ -84,7 +95,8 @@ common_groups(Tests) ->
 		{http_compress, Opts, Tests},
 		{https_compress, Opts, Tests},
 		{h2_compress, Opts, Tests},
-		{h2c_compress, Opts, Tests}
+		{h2c_compress, Opts, Tests},
+		{h3_compress, [], Tests} %% @todo Enable parallel when issues get fixed.
 	].
 
 init_common_groups(Name = http, Config, Mod) ->
@@ -128,7 +140,12 @@ init_common_groups(Name = h2c_compress, Config, Mod) ->
 		env => #{dispatch => Mod:init_dispatch(Config)},
 		stream_handlers => [cowboy_compress_h, cowboy_stream_h]
 	}, [{flavor, compress}|Config]),
-	lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
+	lists:keyreplace(protocol, 1, Config1, {protocol, http2});
+init_common_groups(Name = h3_compress, Config, Mod) ->
+	init_http3(Name, #{
+		env => #{dispatch => Mod:init_dispatch(Config)},
+		stream_handlers => [cowboy_compress_h, cowboy_stream_h]
+	}, [{flavor, compress}|Config]).
 
 %% Support functions for testing using Gun.
 
@@ -138,7 +155,7 @@ gun_open(Config) ->
 gun_open(Config, Opts) ->
 	TlsOpts = case proplists:get_value(no_cert, Config, false) of
 		true -> [{verify, verify_none}];
-		false -> ct_helper:get_certs_from_ets()
+		false -> ct_helper:get_certs_from_ets() %% @todo Wrong in current quicer.
 	end,
 	{ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{
 		retry => 0,

+ 1 - 1
test/loop_handler_SUITE.erl

@@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Dispatch configuration.
 

+ 4 - 2
test/metrics_SUITE.erl

@@ -54,10 +54,12 @@ init_per_group(Name = h2_compress, Config) ->
 	cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
 init_per_group(Name = h2c_compress, Config) ->
 	Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
-	lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
+	lists:keyreplace(protocol, 1, Config1, {protocol, http2});
+init_per_group(Name = h3_compress, Config) ->
+	cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_plain_opts(Config) ->
 	#{

+ 1 - 1
test/misc_SUITE.erl

@@ -43,7 +43,7 @@ init_per_group(Name, Config) ->
 end_per_group(set_env, _) ->
 	ok;
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_dispatch(_) ->
 	cowboy_router:compile([{"localhost", [

+ 1 - 1
test/plain_handler_SUITE.erl

@@ -39,7 +39,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Routes.
 

+ 1 - 1
test/req_SUITE.erl

@@ -46,7 +46,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Routes.
 

+ 1 - 1
test/rest_handler_SUITE.erl

@@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Dispatch configuration.
 

+ 1 - 1
test/rfc6585_SUITE.erl

@@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_dispatch(_) ->
 	cowboy_router:compile([{"[...]", [

+ 21 - 6
test/rfc7231_SUITE.erl

@@ -35,7 +35,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_dispatch(_) ->
 	cowboy_router:compile([{"[...]", [
@@ -237,6 +237,8 @@ http10_expect(Config) ->
 		http ->
 			do_http10_expect(Config);
 		http2 ->
+			expect(Config);
+		http3 ->
 			expect(Config)
 	end.
 
@@ -303,6 +305,9 @@ expect_discard_body_close(Config) ->
 			do_expect_discard_body_close(Config);
 		http2 ->
 			doc("There's no reason to close the connection when using HTTP/2, "
+				"even if a stream body is too large. We just cancel the stream.");
+		http3 ->
+			doc("There's no reason to close the connection when using HTTP/3, "
 				"even if a stream body is too large. We just cancel the stream.")
 	end.
 
@@ -424,8 +429,10 @@ http10_status_code_100(Config) ->
 		http ->
 			doc("The 100 Continue status code must not "
 				"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
-			do_http10_status_code_1xx(100, Config);
+			do_unsupported_status_code_1xx(100, Config);
 		http2 ->
+			status_code_100(Config);
+		http3 ->
 			status_code_100(Config)
 	end.
 
@@ -434,12 +441,16 @@ http10_status_code_101(Config) ->
 		http ->
 			doc("The 101 Switching Protocols status code must not "
 				"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
-			do_http10_status_code_1xx(101, Config);
+			do_unsupported_status_code_1xx(101, Config);
 		http2 ->
+			status_code_101(Config);
+		http3 ->
+			%% While 101 is not supported by HTTP/3, there is no
+			%% wording in RFC9114 that forbids sending it.
 			status_code_101(Config)
 	end.
 
-do_http10_status_code_1xx(StatusCode, Config) ->
+do_unsupported_status_code_1xx(StatusCode, Config) ->
 	ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}),
 	Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [
 		{<<"accept-encoding">>, <<"gzip">>}
@@ -653,7 +664,9 @@ status_code_408_connection_close(Config) ->
 		http ->
 			do_http11_status_code_408_connection_close(Config);
 		http2 ->
-			doc("HTTP/2 connections are not closed on 408 responses.")
+			doc("HTTP/2 connections are not closed on 408 responses.");
+		http3 ->
+			doc("HTTP/3 connections are not closed on 408 responses.")
 	end.
 
 do_http11_status_code_408_connection_close(Config) ->
@@ -744,7 +757,9 @@ status_code_426_upgrade_header(Config) ->
 		http ->
 			do_status_code_426_upgrade_header(Config);
 		http2 ->
-			doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism.")
+			doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism.");
+		http3 ->
+			doc("HTTP/3 does not support the HTTP/1.1 Upgrade mechanism.")
 	end.
 
 do_status_code_426_upgrade_header(Config) ->

+ 1 - 1
test/rfc7538_SUITE.erl

@@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_dispatch(_) ->
 	cowboy_router:compile([{"[...]", [

+ 1 - 1
test/rfc8297_SUITE.erl

@@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 init_dispatch(_) ->
 	cowboy_router:compile([{"[...]", [

+ 2 - 2
test/rfc9114_SUITE.erl

@@ -34,8 +34,8 @@ init_per_group(Name = quic, Config) ->
 		env => #{dispatch => cowboy_router:compile(init_routes(Config))}
 	}, Config).
 
-end_per_group(_Name, _) ->
-	ok. %% @todo = cowboy:stop_listener(Name).
+end_per_group(Name, _) ->
+	cowboy_test:stop_group(Name).
 
 init_routes(_) -> [
 	{"localhost", [

+ 4 - 2
test/security_SUITE.erl

@@ -47,10 +47,12 @@ groups() ->
 		{https, [parallel], Tests ++ H1Tests},
 		{h2, [parallel], Tests},
 		{h2c, [parallel], Tests ++ H2CTests},
+		{h3, [], Tests},
 		{http_compress, [parallel], Tests ++ H1Tests},
 		{https_compress, [parallel], Tests ++ H1Tests},
 		{h2_compress, [parallel], Tests},
-		{h2c_compress, [parallel], Tests ++ H2CTests}
+		{h2c_compress, [parallel], Tests ++ H2CTests},
+		{h3_compress, [], Tests}
 	].
 
 init_per_suite(Config) ->
@@ -64,7 +66,7 @@ init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).
 
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Routes.
 

+ 21 - 3
test/static_handler_SUITE.erl

@@ -39,16 +39,22 @@ groups() ->
 		{dir, [parallel], DirTests},
 		{priv_dir, [parallel], DirTests}
 	],
+	GroupTestsNoParallel = OtherTests ++ [
+		{dir, [], DirTests},
+		{priv_dir, [], DirTests}
+	],
 	[
 		{http, [parallel], GroupTests},
 		{https, [parallel], GroupTests},
 		{h2, [parallel], GroupTests},
 		{h2c, [parallel], GroupTests},
+		{h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
 		{http_compress, [parallel], GroupTests},
 		{https_compress, [parallel], GroupTests},
 		{h2_compress, [parallel], GroupTests},
 		{h2c_compress, [parallel], GroupTests},
-		%% No real need to test sendfile disabled against https or h2.
+		{h3_compress, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
+		%% No real need to test sendfile disabled against https, h2 or h3.
 		{http_no_sendfile, [parallel], GroupTests},
 		{h2c_no_sendfile, [parallel], GroupTests}
 	].
@@ -116,6 +122,17 @@ init_per_group(Name=h2c_no_sendfile, Config) ->
 		sendfile => false
 	}, [{flavor, vanilla}|Config]),
 	lists:keyreplace(protocol, 1, Config1, {protocol, http2});
+init_per_group(Name=h3, Config) ->
+	cowboy_test:init_http3(Name, #{
+		env => #{dispatch => init_dispatch(Config)},
+		middlewares => [?MODULE, cowboy_router, cowboy_handler]
+	}, [{flavor, vanilla}|Config]);
+init_per_group(Name=h3_compress, Config) ->
+	cowboy_test:init_http3(Name, #{
+		env => #{dispatch => init_dispatch(Config)},
+		middlewares => [?MODULE, cowboy_router, cowboy_handler],
+		stream_handlers => [cowboy_compress_h, cowboy_stream_h]
+	}, [{flavor, vanilla}|Config]);
 init_per_group(Name, Config) ->
 	Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE),
 	Opts = ranch:get_protocol_options(Name),
@@ -129,7 +146,7 @@ end_per_group(dir, _) ->
 end_per_group(priv_dir, _) ->
 	ok;
 end_per_group(Name, _) ->
-	cowboy:stop_listener(Name).
+	cowboy_test:stop_group(Name).
 
 %% Large file.
 
@@ -933,7 +950,8 @@ unicode_basic_error(Config) ->
 		%% # and ? indicate fragment and query components
 		%% and are therefore not part of the path.
 		http -> "\r\s#?";
-		http2 -> "#?"
+		http2 -> "#?";
+		http3 -> "#?"
 	end,
 	_ = [case do_get("/char/" ++ [C], Config) of
 		{400, _, _} -> ok;

+ 66 - 59
test/stream_handler_SUITE.erl

@@ -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">>},
@@ -418,21 +416,24 @@ 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 +503,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 +517,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) ->

+ 2 - 1
test/tracer_SUITE.erl

@@ -30,7 +30,8 @@ suite() ->
 %% init_per_suite/1, but this works just as well.
 all() ->
 	cowboy_tracer_h:set_trace_patterns(),
-	cowboy_test:common_all().
+	%% @todo Implement these tests for HTTP/3.
+	cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}].
 
 %% We want tests for each group to execute sequentially
 %% because we need to modify the protocol options. Groups