Browse Source

WIP weekly HTTP/3 work

Loïc Hoguin 1 year ago
parent
commit
d5903ea9ff
4 changed files with 816 additions and 249 deletions
  1. 43 2
      src/cow_http3.erl
  2. 61 3
      src/cow_http3_machine.erl
  3. 55 35
      src/cowboy_http3.erl
  4. 657 209
      test/rfc9114_SUITE.erl

+ 43 - 2
src/cow_http3.erl

@@ -22,6 +22,7 @@
 %% Building.
 -export([data/1]).
 -export([headers/1]).
+-export([settings/1]).
 -export([error_to_code/1]).
 -export([encode_int/1]).
 
@@ -54,6 +55,18 @@ parse(<<0, 3:2, Len:62, Data/bits>>) when byte_size(Data) < Len ->
 %%
 %% HEADERS frames.
 %%
+parse(<<1, 0:2, 0:6, _/bits>>) ->
+	{connection_error, h3_frame_error,
+		'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'};
+parse(<<1, 1:2, 0:14, _/bits>>) ->
+	{connection_error, h3_frame_error,
+		'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'};
+parse(<<1, 2:2, 0:30, _/bits>>) ->
+	{connection_error, h3_frame_error,
+		'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'};
+parse(<<1, 3:2, 0:62, _/bits>>) ->
+	{connection_error, h3_frame_error,
+		'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'};
 parse(<<1, 0:2, Len:6, EncodedFieldSection:Len/binary, Rest/bits>>) ->
 	{ok, {headers, EncodedFieldSection}, Rest};
 parse(<<1, 1:2, Len:14, EncodedFieldSection:Len/binary, Rest/bits>>) ->
@@ -109,6 +122,8 @@ parse(<<7, 0:2, 4:6, 2:2, StreamOrPushID:30, Rest/bits>>) ->
 	{ok, {goaway, StreamOrPushID}, Rest};
 parse(<<7, 0:2, 8:6, 3:2, StreamOrPushID:62, Rest/bits>>) ->
 	{ok, {goaway, StreamOrPushID}, Rest};
+parse(<<7, 0:2, N:6, _/bits>>) when N =:= 1; N =:= 2; N =:= 4; N =:= 8 ->
+	more;
 parse(<<7, _/bits>>) ->
 	{connection_error, h3_frame_error,
 		'GOAWAY frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'};
@@ -123,6 +138,8 @@ parse(<<13, 0:2, 4:6, 2:2, PushID:30, Rest/bits>>) ->
 	{ok, {max_push_id, PushID}, Rest};
 parse(<<13, 0:2, 8:6, 3:2, PushID:62, Rest/bits>>) ->
 	{ok, {max_push_id, PushID}, Rest};
+parse(<<13, 0:2, N:6, _/bits>>) when N =:= 1; N =:= 2; N =:= 4; N =:= 8 ->
+	more;
 parse(<<13, _/bits>>) ->
 	{connection_error, h3_frame_error,
 		'MAX_PUSH_ID frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'};
@@ -287,7 +304,10 @@ code_to_error(16#010c) -> h3_request_cancelled;
 code_to_error(16#010d) -> h3_request_incomplete;
 code_to_error(16#010e) -> h3_message_error;
 code_to_error(16#010f) -> h3_connect_error;
-code_to_error(16#0110) -> h3_version_fallback.
+code_to_error(16#0110) -> h3_version_fallback;
+%% Unknown/reserved error codes must be treated
+%% as equivalent to H3_NO_ERROR.
+code_to_error(_) -> h3_no_error.
 
 %% Building.
 
@@ -303,9 +323,30 @@ headers(HeaderBlock) ->
 	Len = encode_int(iolist_size(HeaderBlock)),
 	[<<1:8>>, Len, HeaderBlock].
 
+-spec settings(_) -> todo.
+
+settings(Settings) when Settings =:= #{} ->
+	<<4:8, 0:8>>;
+settings(Settings) ->
+	Payload = settings_payload(Settings),
+	Len = encode_int(iolist_size(Payload)),
+	[<<4:8>>, Len, Payload].
+
+settings_payload(Settings) ->
+	[case Key of
+		max_header_list_size when Value =:= infinity -> <<>>;
+		max_header_list_size -> [encode_int(6), encode_int(Value)]
+	end || {Key, Value} <- maps:to_list(Settings)].
+
 -spec error_to_code(_) -> todo.
 
-error_to_code(h3_no_error) -> 16#0100;
+error_to_code(h3_no_error) ->
+	%% Implementations should select a reserved error code
+	%% with some probability when they would have sent H3_NO_ERROR. (RFC9114 8.1)
+	case rand:uniform(2) of
+		1 -> 16#0100;
+		2 -> 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21
+	end;
 error_to_code(h3_general_protocol_error) -> 16#0101;
 error_to_code(h3_internal_error) -> 16#0102;
 error_to_code(h3_stream_creation_error) -> 16#0103;

+ 61 - 3
src/cow_http3_machine.erl

@@ -20,6 +20,7 @@
 -export([set_unidi_remote_stream_type/3]).
 -export([close_stream/2]).
 -export([frame/4]).
+-export([ignored_frame/2]).
 -export([prepare_headers/5]).
 -export([reset_stream/2]).
 
@@ -75,7 +76,7 @@
 -spec init(_, _) -> _. %% @todo
 
 init(Mode, _Opts) ->
-	{ok, <<4,0>>, #http3_machine{mode=Mode}}.
+	{ok, cow_http3:settings(#{}), #http3_machine{mode=Mode}}.
 
 -spec init_unidi_local_streams(_, _, _, _, _ ,_ ,_) -> _. %% @todo
 
@@ -130,7 +131,11 @@ frame(Frame, IsFin, StreamRef, State) ->
 	case element(1, Frame) of
 		data -> data_frame(Frame, IsFin, StreamRef, State);
 		headers -> headers_frame(Frame, IsFin, StreamRef, State);
-		settings -> settings_frame(Frame, IsFin, StreamRef, State)
+		cancel_push -> cancel_push_frame(Frame, IsFin, StreamRef, State);
+		settings -> settings_frame(Frame, IsFin, StreamRef, State);
+		push_promise -> push_promise_frame(Frame, IsFin, StreamRef, State);
+		goaway -> goaway_frame(Frame, IsFin, StreamRef, State);
+		max_push_id -> max_push_id_frame(Frame, IsFin, StreamRef, State)
 	end.
 
 %% DATA frame.
@@ -439,19 +444,72 @@ trailers_frame(Stream0, State0=#http3_machine{local_decoder_ref=DecoderRef}, Dec
 %				'The total size of DATA frames is different than the content-length. (RFC7540 8.1.2.6)')
 %	end.
 
+cancel_push_frame(Frame, _IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
+			control_frame(Frame, State)
+	end.
+
 settings_frame(Frame, _IsFin, StreamRef, State) ->
 	case stream_get(StreamRef, State) of
 		#stream{type=control} ->
+			control_frame(Frame, State);
+		#stream{type=req} ->
+			{error, {connection_error, h3_frame_unexpected,
+				'The SETTINGS frame is not allowed on a bidi stream. (RFC9114 7.2.4)'},
+				State}
+	end.
+
+push_promise_frame(Frame, _IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
+			control_frame(Frame, State)
+	end.
+
+goaway_frame(Frame, _IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
+			control_frame(Frame, State)
+	end.
+
+max_push_id_frame(Frame, _IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
 			control_frame(Frame, State)
 	end.
 
 control_frame({settings, _Settings}, State=#http3_machine{has_received_peer_settings=false}) ->
 	{ok, State#http3_machine{has_received_peer_settings=true}};
+control_frame({settings, _}, State) ->
+	{error, {connection_error, h3_frame_unexpected,
+		'The SETTINGS frame cannot be sent more than once. (RFC9114 7.2.4)'},
+		State};
 control_frame(_Frame, State=#http3_machine{has_received_peer_settings=false}) ->
 	{error, {connection_error, h3_missing_settings,
 		'The first frame on the control stream must be a SETTINGS frame. (RFC9114 6.2.1)'},
+		State};
+control_frame(Frame = {goaway, _}, State) ->
+	{ok, Frame, State};
+%% @todo Implement server push.
+control_frame({max_push_id, _}, State) ->
+	{ok, State};
+control_frame(_Frame, State) ->
+	{error, {connection_error, h3_frame_unexpected,
+		'DATA and HEADERS frames are not allowed on the control stream. (RFC9114 7.2.1, RFC9114 7.2.2)'},
 		State}.
-%% @todo control_frame(Frame, State=#http3_machine{has_received_peer_settings=true}) ->
+
+%% Ignored frames.
+
+-spec ignored_frame(_, _) -> _. %% @todo
+
+ignored_frame(StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
+			control_frame(ignored_frame, State);
+		_ ->
+			{ok, State}
+	end.
+
 
 
 %% Functions for sending a message header or body. Note that

+ 55 - 35
src/cowboy_http3.erl

@@ -30,7 +30,7 @@
 	ref :: any(), %% @todo specs
 
 	%% Whether the stream is currently in a special state.
-	status :: header | normal | data | discard, %% @todo What's 'data'?
+	status :: header | normal | {data, non_neg_integer()} | discard,
 
 	%% Stream buffer.
 	buffer = <<>> :: binary(),
@@ -179,22 +179,48 @@ parse(State=#state{streams=Streams, opts=Opts}, Data, StreamRef, Props) ->
 %% @todo Swap Data and Stream/StreamRef.
 parse1(State, Data, Stream=#stream{status=header}, Props) ->
 	parse_unidirectional_stream_header(State, Data, Stream, Props);
-%% @todo Continuation clause for data frames.
+parse1(State, Data, Stream=#stream{status={data, Len}, ref=StreamRef}, Props) ->
+	DataLen = byte_size(Data),
+	if
+		DataLen < Len ->
+			IsFin = is_fin(Props, <<>>),
+			loop(frame(State, Stream#stream{status={data, Len - DataLen}}, {data, Data}, IsFin));
+		true ->
+			<<Data1:Len/binary, Rest/bits>> = Data,
+			IsFin = is_fin(Props, Rest),
+			parse(frame(State, Stream#stream{status=normal}, {data, Data1}, IsFin),
+				Rest, StreamRef, Props)
+	end;
 %% @todo Clause that discards receiving data for aborted streams.
 parse1(State, Data, Stream=#stream{ref=StreamRef}, Props) ->
 	case cow_http3:parse(Data) of
 		{ok, Frame, Rest} ->
 			IsFin = is_fin(Props, Rest),
 			parse(frame(State, Stream, Frame, IsFin), Rest, StreamRef, Props);
-		{more, Frame, _Len} ->
-			%% @todo Change state of stream to expect more data frames.
-			loop(frame(State, Stream, Frame, nofin));
+		{more, Frame, Len} ->
+			IsFin = is_fin(Props, <<>>),
+			case IsFin of
+				nofin ->
+					loop(frame(State, Stream#stream{status={data, Len}}, Frame, nofin));
+				fin ->
+					terminate(State, {connection_error, h3_frame_error,
+						'Last frame on stream was truncated. (RFC9114 7.1)'})
+			end;
 		{ignore, Rest} ->
 			parse(ignored_frame(State, Stream), Rest, StreamRef, Props);
 		Error = {connection_error, _, _} ->
 			terminate(State, Error);
+		more when Data =:= <<>> ->
+			loop(stream_update(State, Stream#stream{buffer=Data}));
 		more ->
-			loop(stream_update(State, Stream#stream{buffer=Data}))
+			IsFin = is_fin(Props, <<>>),
+			case IsFin of
+				nofin ->
+					loop(stream_update(State, Stream#stream{buffer=Data}));
+				fin ->
+					terminate(State, {connection_error, h3_frame_error,
+						'Last frame on stream was truncated. (RFC9114 7.1)'})
+			end
 	end.
 
 %% We may receive multiple frames in a single QUIC packet.
@@ -226,7 +252,7 @@ parse_unidirectional_stream_header(State0=#state{http3_machine=HTTP3Machine0},
 		%% Unknown stream types must be ignored. We choose to abort the
 		%% stream instead of reading and discarding the incoming data.
 		{undefined, _} ->
-			loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error))
+			loop(stream_abort_receive(State0, Stream0, h3_no_error))
 	end.
 
 frame(State=#state{http3_machine=HTTP3Machine0}, Stream=#stream{ref=StreamRef}, Frame, IsFin) ->
@@ -235,6 +261,7 @@ frame(State=#state{http3_machine=HTTP3Machine0}, Stream=#stream{ref=StreamRef},
 			State#state{http3_machine=HTTP3Machine};
 		{ok, {data, Data}, HTTP3Machine} ->
 			data_frame(State#state{http3_machine=HTTP3Machine}, Stream, IsFin, Data);
+		%% @todo I don't think we need the IsFin in the {headers tuple.
 		{ok, {headers, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP3Machine} ->
 			headers_frame(State#state{http3_machine=HTTP3Machine},
 				Stream, IsFin, Headers, PseudoHeaders, BodyLen);
@@ -247,6 +274,8 @@ frame(State=#state{http3_machine=HTTP3Machine0}, Stream=#stream{ref=StreamRef},
 		{ok, {trailers, _Trailers}, HTTP3Machine} ->
 			%% @todo Propagate trailers.
 			State#state{http3_machine=HTTP3Machine};
+		{ok, GoAway={goaway, _}, HTTP3Machine} ->
+			goaway(State#state{http3_machine=HTTP3Machine}, GoAway);
 		{error, Error={stream_error, _Reason, _Human}, HTTP3Machine} ->
 			reset_stream(State#state{http3_machine=HTTP3Machine}, StreamRef, Error);
 		{error, Error={connection_error, _, _}, HTTP3Machine} ->
@@ -467,11 +496,13 @@ commands(State0, StreamRef, [{response, StatusCode, Headers, Body}|Tail]) ->
 %	commands(State, StreamRef, Tail);
 %%% Read the request body.
 %commands(State0=#state{flow=Flow, streams=Streams}, StreamRef, [{flow, Size}|Tail]) ->
+commands(State, StreamRef, [{flow, _Size}|Tail]) ->
+	%% @todo We should tell the QUIC stream to increase its window size.
 %	#{StreamRef := Stream=#stream{flow=StreamFlow}} = Streams,
 %	State = update_window(State0#state{flow=Flow + Size,
 %		streams=Streams#{StreamRef => Stream#stream{flow=StreamFlow + Size}}},
 %		StreamRef),
-%	commands(State, StreamRef, Tail);
+	commands(State, StreamRef, Tail);
 %% Supervise a child process.
 commands(State=#state{children=Children}, StreamRef, [{spawn, Pid, Shutdown}|Tail]) ->
 	 commands(State#state{children=cowboy_children:up(Children, Pid, StreamRef, Shutdown)},
@@ -601,17 +632,24 @@ stop_stream(_, _) ->
 maybe_terminate_stream(_, _, _) ->
 	todo.
 
-%% @todo In ignored_frame we must check for example that the frame
-%%       we received wasn't the first frame in a control stream
-%%       as that one must be SETTINGS.
-ignored_frame(State, _) ->
-	State.
+ignored_frame(State=#state{http3_machine=HTTP3Machine0}, #stream{ref=StreamRef}) ->
+	case cow_http3_machine:ignored_frame(StreamRef, HTTP3Machine0) of
+		{ok, HTTP3Machine} ->
+			State#state{http3_machine=HTTP3Machine};
+		{error, Error={connection_error, _, _}, HTTP3Machine} ->
+			terminate(State#state{http3_machine=HTTP3Machine}, Error)
+	end.
 
 stream_abort_receive(State, Stream=#stream{ref=StreamRef}, Reason) ->
 	quicer:shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE,
-		error_code(Reason), infinity),
+		cow_http3:error_to_code(Reason), infinity),
 	stream_update(State, Stream#stream{status=discard}).
 
+%% @todo Graceful connection shutdown.
+%% We terminate the connection immediately if it hasn't fully been initialized.
+goaway(State, {goaway, _}) ->
+	terminate(State, {stop, goaway, 'The connection is going away.'}).
+
 terminate(State=#state{conn=Conn, %http3_status=Status,
 		%http3_machine=HTTP3Machine,
 		streams=Streams, children=Children}, Reason) ->
@@ -632,8 +670,8 @@ terminate(State=#state{conn=Conn, %http3_status=Status,
 		cow_http3:error_to_code(terminate_reason(Reason))),
 	exit({shutdown, Reason}).
 
-terminate_reason({connection_error, Reason, _}) -> Reason.
-%terminate_reason({stop, _, _}) -> no_error;
+terminate_reason({connection_error, Reason, _}) -> Reason;
+terminate_reason({stop, _, _}) -> h3_no_error.
 %terminate_reason({socket_error, _, _}) -> internal_error;
 %terminate_reason({internal_error, _, _}) -> internal_error.
 
@@ -647,24 +685,6 @@ terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reas
 
 
 
-%% @todo qpack errors
-error_code(h3_no_error) -> 16#0100;
-error_code(h3_general_protocol_error) -> 16#0101;
-error_code(h3_internal_error) -> 16#0102;
-error_code(h3_stream_creation_error) -> 16#0103;
-error_code(h3_closed_critical_stream) -> 16#0104;
-error_code(h3_frame_unexpected) -> 16#0105;
-error_code(h3_frame_error) -> 16#0106;
-error_code(h3_excessive_load) -> 16#0107;
-error_code(h3_id_error) -> 16#0108;
-error_code(h3_settings_error) -> 16#0109;
-error_code(h3_missing_settings) -> 16#010a;
-error_code(h3_request_rejected) -> 16#010b;
-error_code(h3_request_cancelled) -> 16#010c;
-error_code(h3_request_incomplete) -> 16#010d;
-error_code(h3_message_error) -> 16#010e;
-error_code(h3_connect_error) -> 16#010f;
-error_code(h3_version_fallback) -> 16#0110.
 
 stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams}, StreamRef, Flags) ->
 	{ok, StreamID} = quicer:get_stream_id(StreamRef),
@@ -685,7 +705,7 @@ stream_closed(State=#state{http3_machine=HTTP3Machine0, streams=Streams0},
 			%% @todo Some streams may not be bidi or remote.
 			Streams = maps:remove(StreamRef, Streams0),
 			%% @todo terminate stream
-			State#state{streams=Streams};
+			State#state{http3_machine=HTTP3Machine, streams=Streams};
 		{error, Error={connection_error, _, _}, HTTP3Machine} ->
 			terminate(State#state{http3_machine=HTTP3Machine}, Error)
 	end.

+ 657 - 209
test/rfc9114_SUITE.erl

@@ -39,8 +39,8 @@ end_per_group(_Name, _) ->
 
 init_routes(_) -> [
 	{"localhost", [
-		{"/", hello_h, []}%,
-%		{"/echo/:key", echo_h, []},
+		{"/", hello_h, []},
+		{"/echo/:key", echo_h, []}%,
 %		{"/delay_hello", delay_hello_h, 1200},
 %		{"/long_polling", long_polling_h, []},
 %		{"/loop_handler_abort", loop_handler_abort_h, []},
@@ -100,8 +100,7 @@ req_stream(Config) ->
 		<<1>>, %% HEADERS frame.
 		cow_http3:encode_int(iolist_size(EncodedRequest)),
 		EncodedRequest
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	%% Receive the response.
 	{ok, Data} = do_receive_data(StreamRef),
 	{HLenEnc, HLenBits} = do_guess_int_encoding(Data),
@@ -252,8 +251,7 @@ headers_then_trailers(Config) ->
 		<<1>>, %% HEADERS frame for trailers.
 		cow_http3:encode_int(iolist_size(EncodedTrailers)),
 		EncodedTrailers
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -285,8 +283,7 @@ headers_then_data_then_trailers(Config) ->
 		<<1>>, %% HEADERS frame for trailers.
 		cow_http3:encode_int(iolist_size(EncodedTrailers)),
 		EncodedTrailers
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -312,8 +309,7 @@ data_then_headers(Config) ->
 		<<1>>, %% HEADERS frame.
 		cow_http3:encode_int(iolist_size(EncodedHeaders)),
 		EncodedHeaders
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	%% The connection should have been closed.
 	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
 	ok.
@@ -343,8 +339,7 @@ headers_then_trailers_then_data(Config) ->
 		<<0>>, %% DATA frame.
 		cow_http3:encode_int(13),
 		<<"Hello server!">>
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	%% The connection should have been closed.
 	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
 	ok.
@@ -377,8 +372,7 @@ headers_then_data_then_trailers_then_data(Config) ->
 		<<0>>, %% DATA frame.
 		cow_http3:encode_int(13),
 		<<"Hello server!">>
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	%% The connection should have been closed.
 	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
 	ok.
@@ -414,8 +408,7 @@ headers_then_data_then_trailers_then_trailers(Config) ->
 		<<1>>, %% HEADERS frame for trailers.
 		cow_http3:encode_int(iolist_size(EncodedTrailers2)),
 		EncodedTrailers2
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	%% The connection should have been closed.
 	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
 	ok.
@@ -443,8 +436,7 @@ unknown_then_headers(Config, Type, Bytes) ->
 		<<1>>, %% HEADERS frame.
 		cow_http3:encode_int(iolist_size(EncodedHeaders)),
 		EncodedHeaders
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -474,8 +466,7 @@ headers_then_unknown(Config, Type, Bytes) ->
 		cow_http3:encode_int(Type), %% Unknown frame.
 		cow_http3:encode_int(iolist_size(Bytes)),
 		Bytes
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -508,8 +499,7 @@ headers_then_data_then_unknown(Config, Type, Bytes) ->
 		cow_http3:encode_int(Type), %% Unknown frame.
 		cow_http3:encode_int(iolist_size(Bytes)),
 		Bytes
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -545,8 +535,7 @@ headers_then_trailers_then_unknown(Config, Type, Bytes) ->
 		cow_http3:encode_int(Type), %% Unknown frame.
 		cow_http3:encode_int(iolist_size(Bytes)),
 		Bytes
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -586,8 +575,7 @@ headers_then_data_then_unknown_then_trailers(Config, Type, Bytes) ->
 		<<1>>, %% HEADERS frame for trailers.
 		cow_http3:encode_int(iolist_size(EncodedTrailers)),
 		EncodedTrailers
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -624,8 +612,7 @@ headers_then_data_then_unknown_then_data(Config, Type, Bytes) ->
 		<<0>>, %% DATA frame.
 		cow_http3:encode_int(7),
 		<<"server!">>
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -665,8 +652,7 @@ headers_then_data_then_trailers_then_unknown(Config, Type, Bytes) ->
 		cow_http3:encode_int(Type), %% Unknown frame.
 		cow_http3:encode_int(iolist_size(Bytes)),
 		Bytes
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -688,28 +674,28 @@ reserved_then_headers(Config) ->
 	doc("Receipt of reserved frame followed by HEADERS "
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
-	unknown_then_headers(Config, do_reserved_frame_type(),
+	unknown_then_headers(Config, do_reserved_type(),
 		rand:bytes(rand:uniform(4096))).
 
 headers_then_reserved(Config) ->
 	doc("Receipt of HEADERS followed by reserved frame "
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
-	headers_then_unknown(Config, do_reserved_frame_type(),
+	headers_then_unknown(Config, do_reserved_type(),
 		rand:bytes(rand:uniform(4096))).
 
 headers_then_data_then_reserved(Config) ->
 	doc("Receipt of HEADERS followed by DATA followed by reserved frame "
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
-	headers_then_data_then_unknown(Config, do_reserved_frame_type(),
+	headers_then_data_then_unknown(Config, do_reserved_type(),
 		rand:bytes(rand:uniform(4096))).
 
 headers_then_trailers_then_reserved(Config) ->
 	doc("Receipt of HEADERS followed by trailer HEADERS followed by reserved frame "
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
-	headers_then_trailers_then_unknown(Config, do_reserved_frame_type(),
+	headers_then_trailers_then_unknown(Config, do_reserved_type(),
 		rand:bytes(rand:uniform(4096))).
 
 headers_then_data_then_reserved_then_trailers(Config) ->
@@ -718,7 +704,7 @@ headers_then_data_then_reserved_then_trailers(Config) ->
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
 	headers_then_data_then_unknown_then_trailers(Config,
-		do_reserved_frame_type(), rand:bytes(rand:uniform(4096))).
+		do_reserved_type(), rand:bytes(rand:uniform(4096))).
 
 headers_then_data_then_reserved_then_data(Config) ->
 	doc("Receipt of HEADERS followed by DATA followed by "
@@ -726,7 +712,7 @@ headers_then_data_then_reserved_then_data(Config) ->
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
 	headers_then_data_then_unknown_then_data(Config,
-		do_reserved_frame_type(), rand:bytes(rand:uniform(4096))).
+		do_reserved_type(), rand:bytes(rand:uniform(4096))).
 
 headers_then_data_then_trailers_then_reserved(Config) ->
 	doc("Receipt of HEADERS followed by DATA followed by "
@@ -734,9 +720,9 @@ headers_then_data_then_trailers_then_reserved(Config) ->
 		"must be accepted when the reserved frame type is "
 		"of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"),
 	headers_then_data_then_trailers_then_unknown(Config,
-		do_reserved_frame_type(), rand:bytes(rand:uniform(4096))).
+		do_reserved_type(), rand:bytes(rand:uniform(4096))).
 
-do_reserved_frame_type() ->
+do_reserved_type() ->
 	16#1f * (rand:uniform(148764065110560900) - 1) + 16#21.
 
 reject_transfer_encoding_header_with_body(Config) ->
@@ -923,8 +909,7 @@ accept_te_header_value_trailers(Config) ->
 		<<1>>, %% HEADERS frame for trailers.
 		cow_http3:encode_int(iolist_size(EncodedTrailers)),
 		EncodedTrailers
-	]),
-	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -1111,8 +1096,7 @@ accept_host_header_on_missing_pseudo_header_authority(Config) ->
 		<<1>>, %% HEADERS frame.
 		cow_http3:encode_int(iolist_size(EncodedHeaders)),
 		EncodedHeaders
-	]),
-%	ok = do_async_stream_shutdown(StreamRef),
+	], ?QUIC_SEND_FLAG_FIN),
 	#{
 		headers := #{<<":status">> := <<"200">>},
 		body := <<"Hello world!">>
@@ -1219,7 +1203,7 @@ unidi_allow_at_least_three(Config) ->
 		error(timeout)
 	end,
 	%% Confirm that we can create the unidi streams.
-	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
@@ -1294,66 +1278,173 @@ do_accept_qpack_stream(Conn) ->
 %% A receiver MUST tolerate unidirectional streams being closed or reset prior
 %% to the reception of the unidirectional stream header.
 
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-%% A control stream is indicated by a stream type of 0x00. Data on this stream
-%% consists of HTTP/3 frames, as defined in Section 7.2.
-
 %% Each side MUST initiate a single control stream at the beginning of the
 %% connection and send its SETTINGS frame as the first frame on this stream.
 %% @todo What to do when the client never opens a control stream?
 %% @todo Similarly, a stream could be opened but with no data being sent.
 %% @todo Similarly, a control stream could be opened with no SETTINGS frame sent.
 
+control_reject_first_frame_data(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<0>>, %% DATA frame.
+		cow_http3:encode_int(12),
+		<<"Hello world!">>
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
+control_reject_first_frame_headers(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeaders
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
+control_reject_first_frame_cancel_push(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<3>>, %% CANCEL_PUSH frame.
+		cow_http3:encode_int(1),
+		cow_http3:encode_int(0)
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
-
-%% If
-%% the first frame of the control stream is any other frame type, this MUST be
-%% treated as a connection error of type H3_MISSING_SETTINGS.
-
-control_reject_first_frame_data(Config) ->
+control_accept_first_frame_settings(Config) ->
 	doc("The first frame on a control stream "
 		"must be a SETTINGS frame. (RFC9114 6.2.1)"),
 	#{conn := Conn} = do_connect(Config),
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
-	{ok, _} = quicer:send(ControlRef, [<<0>>, <<0, 12, "Hello world!">>]),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin
+	]),
+	%% The connection should remain up.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			Reason = cow_http3:code_to_error(Code),
+			error(Reason)
+	after 1000 ->
+		ok
+	end.
+
+control_reject_first_frame_push_promise(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<5>>, %% PUSH_PROMISE frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders) + 1),
+		cow_http3:encode_int(0),
+		EncodedHeaders
+	]),
 	%% The connection should have been closed.
 	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
 	ok.
 
-%% @todo
-%control_reject_first_frame_headers(Config) ->
-%control_reject_first_frame_cancel_push(Config) ->
-%control_reject_first_frame_push_promise(Config) ->
-%control_accept_first_frame_settings(Config) ->
-%control_reject_first_frame_goaway(Config) ->
-%control_reject_first_frame_max_push_id(Config) ->
-%control_reject_first_frame_reserved(Config) ->
-
-
+control_reject_first_frame_goaway(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<7>>, %% GOAWAY frame.
+		cow_http3:encode_int(1),
+		cow_http3:encode_int(0)
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
+control_reject_first_frame_max_push_id(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<13>>, %% MAX_PUSH_ID frame.
+		cow_http3:encode_int(1),
+		cow_http3:encode_int(0)
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
+control_reject_first_frame_reserved(Config) ->
+	doc("The first frame on a control stream must be a SETTINGS frame "
+		"or the connection must be closed with an H3_MISSING_SETTINGS "
+		"connection error. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	Len = rand:uniform(512),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		cow_http3:encode_int(do_reserved_type()),
+		cow_http3:encode_int(Len),
+		rand:bytes(Len)
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
 
 control_reject_multiple(Config) ->
 	doc("Endpoints must not create multiple control streams. (RFC9114 6.2.1)"),
 	#{conn := Conn} = do_connect(Config),
 	%% Create two control streams.
-	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
 	{ok, ControlRef1} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(ControlRef1, [<<0>>, SettingsBin]),
@@ -1367,7 +1458,7 @@ control_reject_multiple(Config) ->
 control_local_closed_abort(Config) ->
 	doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"),
 	#{conn := Conn} = do_connect(Config),
-	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
@@ -1382,7 +1473,7 @@ control_local_closed_abort(Config) ->
 control_local_closed_graceful(Config) ->
 	doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"),
 	#{conn := Conn} = do_connect(Config),
-	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
@@ -1403,21 +1494,487 @@ control_remote_closed_abort(Config) ->
 
 %% We cannot gracefully shutdown a remote unidi stream; only abort reading.
 
+%% Because the contents of the control stream are used to manage the behavior
+%% of other streams, endpoints SHOULD provide enough flow-control credit to keep
+%% the peer's control stream from becoming blocked.
+
+%% @todo Implement server push (RFC9114 6.2.2 Push Streams)
+
+unidi_accept_reserved_type(Config) ->
+	doc("Endpoints must not consider reserved stream types to have "
+		"any meaning. Reserved streams may be terminated cleanly or "
+		"reset with an H3_NO_ERROR or a reserved error code. (RFC9114 6.2.3)"),
+	#{conn := Conn} = do_connect(Config),
+	%% Create a reserved unidirectional stream.
+	{ok, StreamRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(StreamRef, [
+		cow_http3:encode_int(do_reserved_type()),
+		rand:bytes(rand:uniform(4096))
+	]),
+	%% The stream should have been aborted.
+	#{reason := h3_no_error} = do_wait_stream_aborted(StreamRef),
+	ok.
+
+data_frame_can_span_multiple_packets(Config) ->
+	doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/echo/read_body">>},
+		{<<"content-length">>, <<"13">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeaders,
+		<<0>>, %% DATA frame.
+		cow_http3:encode_int(13),
+		<<"Hello ">>
+	]),
+	timer:sleep(100),
+	{ok, _} = quicer:send(StreamRef, [
+		<<"server!">>
+	], ?QUIC_SEND_FLAG_FIN),
+	#{
+		headers := #{<<":status">> := <<"200">>},
+		body := <<"Hello server!">>
+	} = do_receive_response(StreamRef),
+	ok.
+
+headers_frame_can_span_multiple_packets(Config) ->
+	doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+	Half = iolist_size(EncodedHeaders) div 2,
+	<<EncodedHeadersPart1:Half/binary, EncodedHeadersPart2/bits>>
+		= iolist_to_binary(EncodedHeaders),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeadersPart1
+	]),
+	timer:sleep(100),
+	{ok, _} = quicer:send(StreamRef, [
+		EncodedHeadersPart2
+	]),
+	#{
+		headers := #{<<":status">> := <<"200">>},
+		body := <<"Hello world!">>
+	} = do_receive_response(StreamRef),
+	ok.
+
+%% @todo Implement server push. cancel_push_frame_can_span_multiple_packets(Config) ->
+
+settings_frame_can_span_multiple_packets(Config) ->
+	doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	<<SettingsPart1:1/binary, SettingsPart2/bits>> = SettingsBin,
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsPart1
+	]),
+	timer:sleep(100),
+	{ok, _} = quicer:send(ControlRef, [
+		SettingsPart2
+	]),
+	%% The connection should remain up.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			Reason = cow_http3:code_to_error(Code),
+			error(Reason)
+	after 1000 ->
+		ok
+	end.
+
+goaway_frame_can_span_multiple_packets(Config) ->
+	doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<7>>, cow_http3:encode_int(1) %% GOAWAY part 1.
+	]),
+	timer:sleep(100),
+	{ok, _} = quicer:send(ControlRef, [
+		cow_http3:encode_int(0) %% GOAWAY part 2.
+	]),
+	%% The connection should be closed gracefully.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			h3_no_error = cow_http3:code_to_error(Code),
+			ok
+	after 1000 ->
+		error(timeout)
+	end.
+
+max_push_id_frame_can_span_multiple_packets(Config) ->
+	doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<13>>, cow_http3:encode_int(1) %% MAX_PUSH_ID part 1.
+	]),
+	timer:sleep(100),
+	{ok, _} = quicer:send(ControlRef, [
+		cow_http3:encode_int(0) %% MAX_PUSH_ID part 2.
+	]),
+	%% The connection should remain up.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			Reason = cow_http3:code_to_error(Code),
+			error(Reason)
+	after 1000 ->
+		ok
+	end.
+
+%% The DATA and SETTINGS frames can be zero-length therefore
+%% they cannot be too short.
+
+headers_frame_too_short(Config) ->
+	doc("Frames that terminate before the end of identified fields "
+		"must be rejected with an H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(0)
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+%% @todo Implement server push. cancel_push_frame_too_short(Config) ->
+
+goaway_frame_too_short(Config) ->
+	doc("Frames that terminate before the end of identified fields "
+		"must be rejected with an H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<7>>, cow_http3:encode_int(0) %% GOAWAY.
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+max_push_id_frame_too_short(Config) ->
+	doc("Frames that terminate before the end of identified fields "
+		"must be rejected with an H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<13>>, cow_http3:encode_int(0) %% MAX_PUSH_ID.
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+data_frame_truncated(Config) ->
+	doc("Truncated frames must be rejected with an "
+		"H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/echo/read_body">>},
+		{<<"content-length">>, <<"13">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeaders,
+		<<0>>, %% DATA frame.
+		cow_http3:encode_int(13),
+		<<"Hello ">>
+	], ?QUIC_SEND_FLAG_FIN),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+headers_frame_truncated(Config) ->
+	doc("Truncated frames must be rejected with an "
+		"H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders))
+	], ?QUIC_SEND_FLAG_FIN),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+%% I am not sure how to test truncated CANCEL_PUSH, SETTINGS, GOAWAY
+%% or MAX_PUSH_ID frames, as those are sent on the control stream,
+%% which we cannot terminate.
+
+%% The DATA, HEADERS and SETTINGS frames can be of any length
+%% therefore they cannot be too long per se, even if unwanted
+%% data can be included at the end of the frame's payload.
+
+%% @todo Implement server push. cancel_push_frame_too_long(Config) ->
+
+goaway_frame_too_long(Config) ->
+	doc("Frames that contain additional bytes after the end of identified fields "
+		"must be rejected with an H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<7>>, cow_http3:encode_int(3), %% GOAWAY.
+		<<0, 1, 2>>
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+max_push_id_frame_too_long(Config) ->
+	doc("Frames that contain additional bytes after the end of identified fields "
+		"must be rejected with an H3_FRAME_ERROR connection error. (RFC9114 7.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<13>>, cow_http3:encode_int(9), %% MAX_PUSH_ID.
+		<<0, 1, 2, 3, 4, 5, 6, 7, 8>>
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_error} = do_wait_connection_closed(Conn),
+	ok.
+
+%% Streams may terminate abruptly in the middle of frames.
+
+data_frame_rejected_on_control_stream(Config) ->
+	doc("DATA frames received on the control stream must be rejected "
+		"with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<0>>, %% DATA frame.
+		cow_http3:encode_int(12),
+		<<"Hello world!">>
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
+	ok.
+
+headers_frame_rejected_on_control_stream(Config) ->
+	doc("HEADERS frames received on the control stream must be rejected "
+		"with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.2)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeaders
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
+	ok.
+
+%% @todo Implement server push. (RFC9114 7.2.3. CANCEL_PUSH)
+
+settings_twice(Config) ->
+	doc("Receipt of a second SETTINGS frame on the control stream "
+		"must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.4)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		SettingsBin,
+		SettingsBin
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
+	ok.
+
+settings_on_bidi_stream(Config) ->
+	doc("Receipt of a SETTINGS frame on a bidirectional stream "
+		"must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.4)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+	{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":authority">>, <<"localhost">>},
+		{<<":path">>, <<"/">>},
+		{<<"content-length">>, <<"0">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(StreamRef, [
+		SettingsBin,
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedRequest)),
+		EncodedRequest
+	], ?QUIC_SEND_FLAG_FIN),
+	%% The connection should have been closed.
+	#{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn),
+	ok.
+
+settings_identifier_twice(Config) ->
+	doc("Receipt of a duplicate SETTINGS identifier must be rejected "
+		"with an H3_SETTINGS_ERROR connection error. (RFC9114 7.2.4)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	SettingsPayload = [
+		cow_http3:encode_int(6), cow_http3:encode_int(4096),
+		cow_http3:encode_int(6), cow_http3:encode_int(8192)
+	],
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<4>>, %% SETTINGS frame.
+		cow_http3:encode_int(iolist_size(SettingsPayload)),
+		SettingsPayload
+	]),
+	%% The connection should have been closed.
+	#{reason := h3_settings_error} = do_wait_connection_closed(Conn),
+	ok.
+
+settings_ignore_unknown_identifier(Config) ->
+	doc("Unknown SETTINGS identifiers must be ignored (RFC9114 7.2.4)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	SettingsPayload = [
+		cow_http3:encode_int(999), cow_http3:encode_int(4096)
+	],
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<4>>, %% SETTINGS frame.
+		cow_http3:encode_int(iolist_size(SettingsPayload)),
+		SettingsPayload
+	]),
+	%% The connection should remain up.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			Reason = cow_http3:code_to_error(Code),
+			error(Reason)
+	after 1000 ->
+		ok
+	end.
+
+settings_ignore_reserved_identifier(Config) ->
+	doc("Reserved SETTINGS identifiers must be ignored (RFC9114 7.2.4.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	SettingsPayload = [
+		cow_http3:encode_int(do_reserved_type()), cow_http3:encode_int(4096)
+	],
+	{ok, _} = quicer:send(ControlRef, [
+		<<0>>, %% CONTROL stream.
+		<<4>>, %% SETTINGS frame.
+		cow_http3:encode_int(iolist_size(SettingsPayload)),
+		SettingsPayload
+	]),
+	%% The connection should remain up.
+	receive
+		{quic, shutdown, Conn, {unknown_quic_status, Code}} ->
+			Reason = cow_http3:code_to_error(Code),
+			error(Reason)
+	after 1000 ->
+		ok
+	end.
+
+
+
+
+
+
+
+
+
+
+
+
+
+%% 7.2.4.1. Defined SETTINGS Parameters
+%Endpoints SHOULD include at
+%least one such setting (reserved) in their SETTINGS frame.
+%% -> try sending COW\0 BOY\0 if that fits the encoding and restrictions
+%otherwise something similar
+%% Setting identifiers that were defined in [HTTP/2] where there is no
+%corresponding HTTP/3 setting have also been reserved (Section 11.2.2). These
+%reserved settings MUST NOT be sent, and their receipt MUST be treated as a
+%connection error of type H3_SETTINGS_ERROR.
+
+
+
+
+
+
+
+
+
+
 
 
 
 
 
-%% Because the contents of the control stream are used to manage the behavior
-%of other streams, endpoints SHOULD provide enough flow-control credit to keep
-%the peer's control stream from becoming blocked.
 
-%% 2 control streams => error
-%% no stream type sent (= no control stream)
-%% no settings frame sent
-%% another frame sent instead of settings
-%% close control stream
-%% flow control?
 
 
 
@@ -1487,15 +2044,6 @@ do_guess_int_encoding(Data) ->
 			{3, 62}
 	end.
 
-do_async_stream_shutdown(StreamRef) ->
-	quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0),
-	receive
-		{quic, send_shutdown_complete, StreamRef, true} ->
-			ok
-	after 5000 ->
-		{error, timeout}
-	end.
-
 do_wait_peer_send_shutdown(StreamRef) ->
 	receive
 		{quic, peer_send_shutdown, StreamRef, undefined} ->
@@ -1564,6 +2112,17 @@ do_wait_connection_closed(Conn) ->
 
 
 
+
+
+
+
+
+
+
+
+
+
+
 %% 5.2. Connection Shutdown
 %% Endpoints initiate the graceful shutdown of an HTTP/3 connection by sending
 %a GOAWAY frame. The GOAWAY frame contains an identifier that indicates to the
@@ -1634,117 +2193,6 @@ do_wait_connection_closed(Conn) ->
 
 
 
-%% 6.2.2. Push Streams
-%% A push stream is indicated by a stream type of 0x01, followed by the push ID
-%of the promise that it fulfills, encoded as a variable-length integer. The
-%remaining data on this stream consists of HTTP/3 frames, as defined in Section
-%7.2, and fulfills a promised server push by zero or more interim HTTP
-%responses followed by a single final HTTP response, as defined in Section 4.1.
-%Server push and push IDs are described in Section 4.6.
-%% Only servers can push; if a server receives a client-initiated push stream,
-%this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR.
-%% Each push ID MUST only be used once in a push stream header. If a client
-%detects that a push stream header includes a push ID that was used in another
-%push stream header, the client MUST treat this as a connection error of type
-%H3_ID_ERROR.
-
-%% 6.2.3. Reserved Stream Types
-%% Stream types of the format 0x1f * N + 0x21 for non-negative integer values
-%of N are reserved to exercise the requirement that unknown types be ignored.
-%These streams have no semantics, and they can be sent when application-layer
-%padding is desired. They MAY also be sent on connections where no data is
-%currently being transferred. Endpoints MUST NOT consider these streams to have
-%any meaning upon receipt.
-%% The payload and length of the stream are selected in any manner the sending
-%implementation chooses. When sending a reserved stream type, the
-%implementation MAY either terminate the stream cleanly or reset it. When
-%resetting the stream, either the H3_NO_ERROR error code or a reserved error
-%code (Section 8.1) SHOULD be used.
-
-%% 7. HTTP Framing Layer
-%% Note that, unlike QUIC frames, HTTP/3 frames can span multiple packets.
-
-%% 7.1. Frame Layout
-%% Each frame's payload MUST contain exactly the fields identified in its
-%description. A frame payload that contains additional bytes after the
-%identified fields or a frame payload that terminates before the end of the
-%identified fields MUST be treated as a connection error of type
-%H3_FRAME_ERROR. In particular, redundant length encodings MUST be verified to
-%be self-consistent; see Section 10.8.
-%% When a stream terminates cleanly, if the last frame on the stream was
-%truncated, this MUST be treated as a connection error of type H3_FRAME_ERROR.
-%Streams that terminate abruptly may be reset at any point in a frame.
-
-%% 7.2. Frame Definitions
-%% 7.2.1. DATA
-%% DATA frames MUST be associated with an HTTP request or response. If a DATA
-%frame is received on a control stream, the recipient MUST respond with a
-%connection error of type H3_FRAME_UNEXPECTED.
-
-%% 7.2.2. HEADERS
-%% HEADERS frames can only be sent on request streams or push streams. If a
-%HEADERS frame is received on a control stream, the recipient MUST respond with
-%a connection error of type H3_FRAME_UNEXPECTED.
-
-%% 7.2.3. CANCEL_PUSH
-%% When a client sends a CANCEL_PUSH frame, it is indicating that it does not
-%wish to receive the promised resource. The server SHOULD abort sending the
-%resource, but the mechanism to do so depends on the state of the corresponding
-%push stream. If the server has not yet created a push stream, it does not
-%create one. If the push stream is open, the server SHOULD abruptly terminate
-%that stream. If the push stream has already ended, the server MAY still
-%abruptly terminate the stream or MAY take no action.
-%% A server sends a CANCEL_PUSH frame to indicate that it will not be
-%fulfilling a promise that was previously sent. The client cannot expect the
-%corresponding promise to be fulfilled, unless it has already received and
-%processed the promised response. Regardless of whether a push stream has been
-%opened, a server SHOULD send a CANCEL_PUSH frame when it determines that
-%promise will not be fulfilled. If a stream has already been opened, the server
-%can abort sending on the stream with an error code of H3_REQUEST_CANCELLED.
-%% Sending a CANCEL_PUSH frame has no direct effect on the state of existing
-%push streams. A client SHOULD NOT send a CANCEL_PUSH frame when it has already
-%received a corresponding push stream. A push stream could arrive after a
-%client has sent a CANCEL_PUSH frame, because a server might not have processed
-%the CANCEL_PUSH. The client SHOULD abort reading the stream with an error code
-%of H3_REQUEST_CANCELLED.
-%% A CANCEL_PUSH frame is sent on the control stream. Receiving a CANCEL_PUSH
-%frame on a stream other than the control stream MUST be treated as a
-%connection error of type H3_FRAME_UNEXPECTED.
-%% If a CANCEL_PUSH frame is received that references a push ID greater than
-%currently allowed on the connection, this MUST be treated as a connection
-%error of type H3_ID_ERROR.
-%% If the client receives a CANCEL_PUSH frame, that frame might identify a push
-%ID that has not yet been mentioned by a PUSH_PROMISE frame due to reordering.
-%If a server receives a CANCEL_PUSH frame for a push ID that has not yet been
-%mentioned by a PUSH_PROMISE frame, this MUST be treated as a connection error
-%of type H3_ID_ERROR.
-
-%% 7.2.4. SETTINGS
-%% A SETTINGS frame MUST be sent as the first frame of each control stream (see
-%Section 6.2.1) by each peer, and it MUST NOT be sent subsequently. If an
-%endpoint receives a second SETTINGS frame on the control stream, the endpoint
-%MUST respond with a connection error of type H3_FRAME_UNEXPECTED.
-%% SETTINGS frames MUST NOT be sent on any stream other than the control
-%stream. If an endpoint receives a SETTINGS frame on a different stream, the
-%endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED.
-%% The same setting identifier MUST NOT occur more than once in the SETTINGS
-%frame. A receiver MAY treat the presence of duplicate setting identifiers as a
-%connection error of type H3_SETTINGS_ERROR.
-%% An implementation MUST ignore any parameter with an identifier it does not understand.
-
-%% 7.2.4.1. Defined SETTINGS Parameters
-%% Setting identifiers of the format 0x1f * N + 0x21 for non-negative integer
-%values of N are reserved to exercise the requirement that unknown identifiers
-%be ignored. Such settings have no defined meaning. Endpoints SHOULD include at
-%least one such setting in their SETTINGS frame. Endpoints MUST NOT consider
-%such settings to have any meaning upon receipt.
-%% -> try sending COW\0 BOY\0 if that fits the encoding and restrictions
-%otherwise something similar
-%% Setting identifiers that were defined in [HTTP/2] where there is no
-%corresponding HTTP/3 setting have also been reserved (Section 11.2.2). These
-%reserved settings MUST NOT be sent, and their receipt MUST be treated as a
-%connection error of type H3_SETTINGS_ERROR.
-
 %% 7.2.4.2. Initialization
 %% An HTTP implementation MUST NOT send frames or requests that would be
 %invalid based on its current understanding of the peer's settings.