From 34652ed32136f67600d2deb4d4c1d07ef5cf84f5 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 12 Aug 2026 13:43:59 -0400 Subject: [PATCH] refactor: document more duvet compliance --- .duvet/snapshot.txt | 48 ++++++++++++++--------------- .duvet/todos/rfc9114/4.1.2.toml | 6 ---- .duvet/todos/rfc9114/4.1.toml | 7 ----- .duvet/todos/rfc9114/4.3.toml | 14 --------- .duvet/todos/rfc9114/6.2.toml | 7 ----- .duvet/todos/rfc9114/7.2.8.toml | 15 --------- .duvet/todos/rfc9114/9.toml | 7 ----- h3/src/client/stream.rs | 6 ++++ h3/src/connection.rs | 10 ++++++ h3/src/frame.rs | 4 +++ h3/src/proto/headers.rs | 54 +++++++++++++++++++++++++++++++++ h3/src/tests/request.rs | 8 +++-- 12 files changed, 102 insertions(+), 84 deletions(-) delete mode 100644 .duvet/todos/rfc9114/4.3.toml delete mode 100644 .duvet/todos/rfc9114/7.2.8.toml diff --git a/.duvet/snapshot.txt b/.duvet/snapshot.txt index a11d810f..f0364a1f 100644 --- a/.duvet/snapshot.txt +++ b/.duvet/snapshot.txt @@ -89,9 +89,9 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,todo]: PUSH_PROMISE frames are not permitted on push streams; TEXT[!MUST,todo]: a pushed response that includes PUSH_PROMISE frames MUST be treated TEXT[!MUST,todo]: as a connection error of type H3_FRAME_UNEXPECTED. - TEXT[!MAY,todo]: Frames of unknown types (Section 9), including reserved frames - TEXT[!MAY,todo]: (Section 7.2.8) MAY be sent on a request or push stream before, - TEXT[!MAY,todo]: after, or interleaved with other frames described in this section. + TEXT[!MAY,implementation,test]: Frames of unknown types (Section 9), including reserved frames + TEXT[!MAY,implementation,test]: (Section 7.2.8) MAY be sent on a request or push stream before, + TEXT[!MAY,implementation,test]: after, or interleaved with other frames described in this section. TEXT[!MUST,todo]: Transfer codings (see Section 7 of [HTTP/1.1]) are not defined for TEXT[!MUST,todo]: HTTP/3; the Transfer-Encoding header field MUST NOT be used. TEXT[!MAY,todo]: A response MAY consist of multiple messages when and only when one or @@ -170,12 +170,12 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,exception]: Intermediaries that process HTTP requests or responses (i.e., any TEXT[!MUST,exception]: intermediary not acting as a tunnel) MUST NOT forward a malformed TEXT[!MUST,exception]: request or response. - TEXT[!MUST,implementation,todo]: Malformed requests or responses that are - TEXT[!MUST,implementation,todo]: detected MUST be treated as a stream error of type H3_MESSAGE_ERROR. + TEXT[!MUST,implementation]: Malformed requests or responses that are + TEXT[!MUST,implementation]: detected MUST be treated as a stream error of type H3_MESSAGE_ERROR. TEXT[!MAY,todo]: For malformed requests, a server MAY send an HTTP response indicating TEXT[!MAY,todo]: the error prior to closing or resetting the stream. - TEXT[!MUST,todo]: Clients MUST NOT - TEXT[!MUST,todo]: accept a malformed response. + TEXT[!MUST,implementation,todo]: Clients MUST NOT + TEXT[!MUST,implementation,todo]: accept a malformed response. SECTION: [HTTP Fields](#section-4.2) TEXT[!MUST,todo]: Characters in field names MUST be @@ -223,13 +223,13 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,implication]: appear in requests. TEXT[!MUST,implementation]: Pseudo-header fields MUST NOT appear in trailer TEXT[!MUST,implementation]: sections. - TEXT[!MUST,todo]: Endpoints MUST treat a request or response that contains - TEXT[!MUST,todo]: undefined or invalid pseudo-header fields as malformed. + TEXT[!MUST,implementation,test]: Endpoints MUST treat a request or response that contains + TEXT[!MUST,implementation,test]: undefined or invalid pseudo-header fields as malformed. TEXT[!MUST,implementation]: All pseudo-header fields MUST appear in the header section before TEXT[!MUST,implementation]: regular header fields. - TEXT[!MUST,todo]: Any request or response that contains a - TEXT[!MUST,todo]: pseudo-header field that appears in a header section after a regular - TEXT[!MUST,todo]: header field MUST be treated as malformed. + TEXT[!MUST,implementation,test]: Any request or response that contains a + TEXT[!MUST,implementation,test]: pseudo-header field that appears in a header section after a regular + TEXT[!MUST,implementation,test]: header field MUST be treated as malformed. SECTION: [Request Pseudo-Header Fields](#section-4.3.1) TEXT[!MUST,todo]: The authority MUST NOT include the @@ -463,9 +463,9 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,todo]: However, stream types that could modify the state or TEXT[!MUST,todo]: semantics of existing protocol components, including QPACK or other TEXT[!MUST,todo]: extensions, MUST NOT be sent until the peer is known to support them. - TEXT[!MUST,implementation,todo]: A receiver MUST tolerate unidirectional streams being - TEXT[!MUST,implementation,todo]: closed or reset prior to the reception of the unidirectional stream - TEXT[!MUST,implementation,todo]: header. + TEXT[!MUST,implementation]: A receiver MUST tolerate unidirectional streams being + TEXT[!MUST,implementation]: closed or reset prior to the reception of the unidirectional stream + TEXT[!MUST,implementation]: header. SECTION: [Control Streams](#section-6.2.1) TEXT[!MUST,implementation]: Each side MUST initiate a single control stream at the beginning of @@ -715,13 +715,11 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[implication]: types be ignored (Section 9). TEXT[!MAY,implication]: These frames have no semantics, and TEXT[!MAY,implication]: they MAY be sent on any stream where frames are allowed to be sent. - TEXT[!MUST,implementation,todo]: Endpoints MUST - TEXT[!MUST,implementation,todo]: NOT consider these frames to have any meaning upon receipt. - TEXT[todo]: Frame types that were used in HTTP/2 where there is no corresponding - TEXT[todo]: HTTP/3 frame have also been reserved (Section 11.2.1). - TEXT[!MUST,implementation,todo]: These frame - TEXT[!MUST,implementation,todo]: types MUST NOT be sent, and their receipt MUST be treated as a - TEXT[!MUST,implementation,todo]: connection error of type H3_FRAME_UNEXPECTED. + TEXT[!MUST,implementation]: Endpoints MUST + TEXT[!MUST,implementation]: NOT consider these frames to have any meaning upon receipt. + TEXT[!MUST,implementation]: These frame + TEXT[!MUST,implementation]: types MUST NOT be sent, and their receipt MUST be treated as a + TEXT[!MUST,implementation]: connection error of type H3_FRAME_UNEXPECTED. SECTION: [Error Handling](#section-8) TEXT[!MAY,todo]: An endpoint MAY choose to treat a stream error as a connection error @@ -742,9 +740,9 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 SECTION: [Extensions to HTTP/3](#section-9) TEXT[!MUST,todo]: Implementations MUST ignore unknown or unsupported values in all TEXT[!MUST,todo]: extensible protocol elements. - TEXT[!MUST,todo]: Implementations MUST discard data or - TEXT[!MUST,todo]: abort reading on unidirectional streams that have unknown or - TEXT[!MUST,todo]: unsupported types. + TEXT[!MUST,implementation]: Implementations MUST discard data or + TEXT[!MUST,implementation]: abort reading on unidirectional streams that have unknown or + TEXT[!MUST,implementation]: unsupported types. TEXT[!SHOULD,todo]: However, where a known frame type is required to be in TEXT[!SHOULD,todo]: a specific location, such as the SETTINGS frame as the first frame of TEXT[!SHOULD,todo]: the control stream (see Section 6.2.1), an unknown frame type does diff --git a/.duvet/todos/rfc9114/4.1.2.toml b/.duvet/todos/rfc9114/4.1.2.toml index b138ce59..e8b082c9 100644 --- a/.duvet/todos/rfc9114/4.1.2.toml +++ b/.duvet/todos/rfc9114/4.1.2.toml @@ -1,11 +1,5 @@ target = "https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2" -[[TODO]] -quote = ''' -Malformed requests or responses that are -detected MUST be treated as a stream error of type H3_MESSAGE_ERROR. -''' - [[TODO]] quote = ''' For malformed requests, a server MAY send an HTTP response indicating diff --git a/.duvet/todos/rfc9114/4.1.toml b/.duvet/todos/rfc9114/4.1.toml index cac141dd..f601d193 100644 --- a/.duvet/todos/rfc9114/4.1.toml +++ b/.duvet/todos/rfc9114/4.1.toml @@ -20,13 +20,6 @@ a pushed response that includes PUSH_PROMISE frames MUST be treated as a connection error of type H3_FRAME_UNEXPECTED. ''' -[[TODO]] -quote = ''' -Frames of unknown types (Section 9), including reserved frames -(Section 7.2.8) MAY be sent on a request or push stream before, -after, or interleaved with other frames described in this section. -''' - [[TODO]] quote = ''' Transfer codings (see Section 7 of [HTTP/1.1]) are not defined for diff --git a/.duvet/todos/rfc9114/4.3.toml b/.duvet/todos/rfc9114/4.3.toml deleted file mode 100644 index cd4bef3e..00000000 --- a/.duvet/todos/rfc9114/4.3.toml +++ /dev/null @@ -1,14 +0,0 @@ -target = "https://www.rfc-editor.org/rfc/rfc9114#section-4.3" - -[[TODO]] -quote = ''' -Endpoints MUST treat a request or response that contains -undefined or invalid pseudo-header fields as malformed. -''' - -[[TODO]] -quote = ''' -Any request or response that contains a -pseudo-header field that appears in a header section after a regular -header field MUST be treated as malformed. -''' diff --git a/.duvet/todos/rfc9114/6.2.toml b/.duvet/todos/rfc9114/6.2.toml index 37ea5129..46f1e3cd 100644 --- a/.duvet/todos/rfc9114/6.2.toml +++ b/.duvet/todos/rfc9114/6.2.toml @@ -12,10 +12,3 @@ However, stream types that could modify the state or semantics of existing protocol components, including QPACK or other extensions, MUST NOT be sent until the peer is known to support them. ''' - -[[TODO]] -quote = ''' -A receiver MUST tolerate unidirectional streams being -closed or reset prior to the reception of the unidirectional stream -header. -''' diff --git a/.duvet/todos/rfc9114/7.2.8.toml b/.duvet/todos/rfc9114/7.2.8.toml deleted file mode 100644 index 1ec5d9f7..00000000 --- a/.duvet/todos/rfc9114/7.2.8.toml +++ /dev/null @@ -1,15 +0,0 @@ -target = "https://www.rfc-editor.org/rfc/rfc9114#section-7.2.8" - -[[TODO]] -quote = ''' -Endpoints MUST -NOT consider these frames to have any meaning upon receipt. -''' - -[[TODO]] -quote = ''' -Frame types that were used in HTTP/2 where there is no corresponding -HTTP/3 frame have also been reserved (Section 11.2.1). These frame -types MUST NOT be sent, and their receipt MUST be treated as a -connection error of type H3_FRAME_UNEXPECTED. -''' diff --git a/.duvet/todos/rfc9114/9.toml b/.duvet/todos/rfc9114/9.toml index aff73159..c4da190b 100644 --- a/.duvet/todos/rfc9114/9.toml +++ b/.duvet/todos/rfc9114/9.toml @@ -6,13 +6,6 @@ Implementations MUST ignore unknown or unsupported values in all extensible protocol elements. ''' -[[TODO]] -quote = ''' -Implementations MUST discard data or -abort reading on unidirectional streams that have unknown or -unsupported types. -''' - [[TODO]] quote = ''' However, where a known frame type is required to be in diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index 1c69fe03..b1a9641f 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -160,6 +160,12 @@ where let qpack::Decoded { fields, .. } = decoded; + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2 + //# Malformed requests or responses that are + //# detected MUST be treated as a stream error of type H3_MESSAGE_ERROR. + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2 + //# Clients MUST NOT + //# accept a malformed response. let (status, headers) = Header::try_from(fields) .map_err(|_e| { self.inner.stream.stop_sending(Code::H3_REQUEST_CANCELLED); diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 4a98ddd9..44302f1a 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -473,6 +473,10 @@ where //# Endpoints MUST NOT consider these streams to have any meaning upon //# receipt. AcceptedRecvStream::Unknown(mut stream) => { + //= https://www.rfc-editor.org/rfc/rfc9114#section-9 + //# Implementations MUST discard data or + //# abort reading on unidirectional streams that have unknown or + //# unsupported types. //= https://www.rfc-editor.org/rfc/rfc9114#section-6.2 //# Recipients of unknown stream types MUST //# either abort reading of the stream or discard incoming data without @@ -1011,6 +1015,12 @@ where Poll::Ready(Ok(Some( Header::try_from(fields) .map_err(|_e| { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2 + //# Malformed requests or responses that are + //# detected MUST be treated as a stream error of type H3_MESSAGE_ERROR. + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2 + //# Clients MUST NOT + //# accept a malformed response. self.stop_sending(Code::H3_MESSAGE_ERROR); StreamError::StreamError { code: Code::H3_MESSAGE_ERROR, diff --git a/h3/src/frame.rs b/h3/src/frame.rs index cc3245b7..f9efc57b 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -243,6 +243,10 @@ impl FrameDecoder { match decoded { Err(frame::FrameError::UnknownFrame(_ty)) => { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1 + //# Frames of unknown types (Section 9), including reserved frames + //# (Section 7.2.8) MAY be sent on a request or push stream before, + //# after, or interleaved with other frames described in this section. //= https://www.rfc-editor.org/rfc/rfc9114#section-7.2.8 //# Endpoints MUST //# NOT consider these frames to have any meaning upon receipt. diff --git a/h3/src/proto/headers.rs b/h3/src/proto/headers.rs index d3736c64..d84ed0ed 100644 --- a/h3/src/proto/headers.rs +++ b/h3/src/proto/headers.rs @@ -206,10 +206,25 @@ impl TryFrom> for Header { fn try_from(headers: Vec) -> Result { let mut fields = HeaderMap::with_capacity(headers.len()); let mut pseudo = Pseudo::default(); + let mut regular_field_seen = false; for field in headers.into_iter() { let (name, value) = field.into_inner(); match Field::parse(name, value)? { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.3 + //# Any request or response that contains a + //# pseudo-header field that appears in a header section after a regular + //# header field MUST be treated as malformed. + Field::Method(_) + | Field::Scheme(_) + | Field::Authority(_) + | Field::Path(_) + | Field::Status(_) + | Field::Protocol(_) + if regular_field_seen => + { + return Err(HeaderError::PseudoAfterRegularField) + } Field::Method(m) => { pseudo.method = Some(m); pseudo.len += 1; @@ -231,6 +246,7 @@ impl TryFrom> for Header { pseudo.len += 1; } Field::Header((n, v)) => { + regular_field_seen = true; fields.append(n, v); } Field::Protocol(p) => { @@ -304,6 +320,9 @@ impl Field { .map_err(|_| HeaderError::invalid_value(name, value))?, ), b":protocol" => Field::Protocol(try_value(name, value)?), + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.3 + //# Endpoints MUST treat a request or response that contains + //# undefined or invalid pseudo-header fields as malformed. _ => return Err(HeaderError::invalid_name(name)), }) } @@ -446,6 +465,7 @@ pub enum HeaderError { MissingStatus, MissingAuthority, ContradictedAuthority, + PseudoAfterRegularField, } impl HeaderError { @@ -483,6 +503,12 @@ impl fmt::Display for HeaderError { HeaderError::ContradictedAuthority => { write!(f, "uri and authority field are in contradiction") } + HeaderError::PseudoAfterRegularField => { + write!( + f, + "pseudo-header field appears after a regular header field" + ) + } } } } @@ -642,4 +668,32 @@ mod tests { },] ); } + + #[test] + fn rejects_undefined_pseudo_header() { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.3 + //= type=test + //# Endpoints MUST treat a request or response that contains + //# undefined or invalid pseudo-header fields as malformed. + assert_matches!( + Header::try_from(vec![(b":unknown", b"value").into()]), + Err(HeaderError::InvalidHeaderName(_)) + ); + } + + #[test] + fn rejects_pseudo_header_after_regular_header() { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.3 + //= type=test + //# Any request or response that contains a + //# pseudo-header field that appears in a header section after a regular + //# header field MUST be treated as malformed. + assert_matches!( + Header::try_from(vec![ + (b"regular", b"value").into(), + (b":method", b"GET").into(), + ]), + Err(HeaderError::PseudoAfterRegularField) + ); + } } diff --git a/h3/src/tests/request.rs b/h3/src/tests/request.rs index 09796f4a..e150ec6a 100644 --- a/h3/src/tests/request.rs +++ b/h3/src/tests/request.rs @@ -1105,9 +1105,11 @@ async fn request_valid_header_trailer() { .await; } -// Frames of unknown types (Section 9), including reserved frames (Section -// 7.2.8) MAY be sent on a request or push stream before, after, or interleaved -// with other frames described in this section. +//= https://www.rfc-editor.org/rfc/rfc9114#section-4.1 +//= type=test +//# Frames of unknown types (Section 9), including reserved frames +//# (Section 7.2.8) MAY be sent on a request or push stream before, +//# after, or interleaved with other frames described in this section. #[tokio::test] async fn request_valid_unknown_frame_before() {