Skip to main content

trillium_http/h3/
error.rs

1use std::borrow::Cow;
2
3/// H3 error codes per RFC 9114 §8.1.
4///
5/// Used when closing connections or resetting streams.
6/// Unknown error codes are mapped to `NoError` per spec.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
8#[non_exhaustive]
9pub enum H3ErrorCode {
10    /// No error. Used when closing without an error to signal.
11    #[error("No error. Used when closing without an error to signal.")]
12    NoError = 0x0100,
13
14    /// Peer violated protocol requirements.
15    #[error("Peer violated protocol requirements.")]
16    GeneralProtocolError = 0x0101,
17
18    /// An internal error in the HTTP stack.
19    #[error("An internal error in the HTTP stack.")]
20    InternalError = 0x0102,
21
22    /// Peer created a stream that will not be accepted.
23    #[error("Peer created a stream that will not be accepted.")]
24    StreamCreationError = 0x0103,
25
26    /// A required stream was closed or reset.
27    #[error("A required stream was closed or reset.")]
28    ClosedCriticalStream = 0x0104,
29
30    /// A frame was not permitted in the current state or stream.
31    #[error("A frame was not permitted in the current state or stream.")]
32    FrameUnexpected = 0x0105,
33
34    /// A frame fails layout requirements or has an invalid size.
35    #[error("A frame fails layout requirements or has an invalid size.")]
36    FrameError = 0x0106,
37
38    /// Peer is generating excessive load.
39    #[error("Peer is generating excessive load.")]
40    ExcessiveLoad = 0x0107,
41
42    /// A stream ID or push ID was used incorrectly.
43    #[error("A stream ID or push ID was used incorrectly.")]
44    IdError = 0x0108,
45
46    /// Error in the payload of a SETTINGS frame.
47    #[error("Error in the payload of a SETTINGS frame.")]
48    SettingsError = 0x0109,
49
50    /// No SETTINGS frame at the beginning of the control stream.
51    #[error("No SETTINGS frame at the beginning of the control stream.")]
52    MissingSettings = 0x010a,
53
54    /// Server rejected a request without application processing.
55    #[error("Server rejected a request without application processing.")]
56    RequestRejected = 0x010b,
57
58    /// Request or response (including pushed) is cancelled.
59    #[error("Request or response (including pushed) is cancelled.")]
60    RequestCancelled = 0x010c,
61
62    /// Client stream terminated without a fully formed request.
63    #[error("Client stream terminated without a fully formed request.")]
64    RequestIncomplete = 0x010d,
65
66    /// HTTP message was malformed.
67    #[error("HTTP message was malformed.")]
68    MessageError = 0x010e,
69
70    /// TCP connection for CONNECT was reset or abnormally closed.
71    #[error("TCP connection for CONNECT was reset or abnormally closed.")]
72    ConnectError = 0x010f,
73
74    /// Requested operation cannot be served over HTTP/3.
75    #[error("Requested operation cannot be served over HTTP/3.")]
76    VersionFallback = 0x0110,
77
78    // -- WebTransport error codes (draft-ietf-webtrans-http3) --
79    /// WebTransport data stream rejected due to lack of associated session.
80    #[error("WebTransport data stream rejected due to lack of associated session.")]
81    WebTransportBufferedStreamRejected = 0x3994_bd84,
82
83    /// WebTransport data stream or session closed because the associated session is gone.
84    #[error("WebTransport session gone.")]
85    WebTransportSessionGone = 0x170d_7b68,
86
87    /// WebTransport session flow control error.
88    #[error("WebTransport flow control error.")]
89    WebTransportFlowControlError = 0x045d_4487,
90
91    /// WebTransport application protocol negotiation failed.
92    #[error("WebTransport ALPN error.")]
93    WebTransportAlpnError = 0x0817_b3dd,
94
95    /// Required WebTransport settings or transport parameters not met.
96    #[error("WebTransport requirements not met.")]
97    WebTransportRequirementsNotMet = 0x212c_0d48,
98
99    // -- QPACK error codes (RFC 9204 §6) --
100    /// The decoder failed to interpret a header block.
101    #[error("QPACK decompression failed.")]
102    QpackDecompressionFailed = 0x200,
103
104    /// The decoder failed to interpret an encoder stream instruction.
105    #[error("QPACK encoder stream error.")]
106    QpackEncoderStreamError = 0x201,
107
108    /// The encoder failed to interpret a decoder stream instruction.
109    #[error("QPACK decoder stream error.")]
110    QpackDecoderStreamError = 0x202,
111}
112
113impl H3ErrorCode {
114    /// A "reason phrase" per rfc9000 §19.19
115    pub fn reason(&self) -> Cow<'static, str> {
116        // eventually this probably should either be &'static str or callsite-specific
117        Cow::Owned(format!("{self}"))
118    }
119
120    /// Returns `true` if this error code represents a connection-level error that requires
121    /// closing the entire QUIC connection (via `CONNECTION_CLOSE`).
122    ///
123    /// Returns `false` for stream-level errors such as [`Self::MessageError`] or
124    /// [`Self::RequestIncomplete`], which should reset the individual stream rather than
125    /// tear down the whole connection.
126    pub fn is_connection_error(&self) -> bool {
127        matches!(
128            self,
129            Self::GeneralProtocolError
130                | Self::InternalError
131                | Self::ClosedCriticalStream
132                | Self::FrameUnexpected
133                | Self::FrameError
134                | Self::ExcessiveLoad
135                | Self::IdError
136                | Self::SettingsError
137                | Self::MissingSettings
138        )
139    }
140}
141
142impl From<u64> for H3ErrorCode {
143    /// All unknown error codes are treated as equivalent to `NoError`
144    /// per RFC 9114 §9.
145    fn from(value: u64) -> Self {
146        match value {
147            0x0101 => Self::GeneralProtocolError,
148            0x0102 => Self::InternalError,
149            0x0103 => Self::StreamCreationError,
150            0x0104 => Self::ClosedCriticalStream,
151            0x0105 => Self::FrameUnexpected,
152            0x0106 => Self::FrameError,
153            0x0107 => Self::ExcessiveLoad,
154            0x0108 => Self::IdError,
155            0x0109 => Self::SettingsError,
156            0x010a => Self::MissingSettings,
157            0x010b => Self::RequestRejected,
158            0x010c => Self::RequestCancelled,
159            0x010d => Self::RequestIncomplete,
160            0x010e => Self::MessageError,
161            0x010f => Self::ConnectError,
162            0x0110 => Self::VersionFallback,
163            0x3994_bd84 => Self::WebTransportBufferedStreamRejected,
164            0x170d_7b68 => Self::WebTransportSessionGone,
165            0x045d_4487 => Self::WebTransportFlowControlError,
166            0x0817_b3dd => Self::WebTransportAlpnError,
167            0x212c_0d48 => Self::WebTransportRequirementsNotMet,
168            0x200 => Self::QpackDecompressionFailed,
169            0x201 => Self::QpackEncoderStreamError,
170            0x202 => Self::QpackDecoderStreamError,
171            _ => Self::NoError,
172        }
173    }
174}
175
176impl From<H3ErrorCode> for u64 {
177    /// Encodes the error code. `NoError` emits a random GREASE value
178    /// (`0x1f * N + 0x21`) per RFC 9114 §8.1 to exercise peer handling
179    /// of unknown codes.
180    fn from(code: H3ErrorCode) -> u64 {
181        match code {
182            H3ErrorCode::NoError => {
183                let n = u64::from(fastrand::u16(..));
184                0x1f * n + 0x21
185            }
186            other => other as u64,
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn known_codes_roundtrip() {
197        for code in [
198            H3ErrorCode::GeneralProtocolError,
199            H3ErrorCode::InternalError,
200            H3ErrorCode::StreamCreationError,
201            H3ErrorCode::ClosedCriticalStream,
202            H3ErrorCode::FrameUnexpected,
203            H3ErrorCode::FrameError,
204            H3ErrorCode::ExcessiveLoad,
205            H3ErrorCode::IdError,
206            H3ErrorCode::SettingsError,
207            H3ErrorCode::MissingSettings,
208            H3ErrorCode::RequestRejected,
209            H3ErrorCode::RequestCancelled,
210            H3ErrorCode::RequestIncomplete,
211            H3ErrorCode::MessageError,
212            H3ErrorCode::ConnectError,
213            H3ErrorCode::VersionFallback,
214            H3ErrorCode::WebTransportBufferedStreamRejected,
215            H3ErrorCode::WebTransportSessionGone,
216            H3ErrorCode::WebTransportFlowControlError,
217            H3ErrorCode::WebTransportAlpnError,
218            H3ErrorCode::WebTransportRequirementsNotMet,
219            H3ErrorCode::QpackDecompressionFailed,
220            H3ErrorCode::QpackEncoderStreamError,
221            H3ErrorCode::QpackDecoderStreamError,
222        ] {
223            let wire: u64 = code.into();
224            let decoded = H3ErrorCode::from(wire);
225            assert_eq!(decoded, code, "roundtrip failed for {code:?}");
226        }
227    }
228
229    #[test]
230    fn no_error_encodes_as_grease() {
231        for _ in 0..100 {
232            let wire: u64 = H3ErrorCode::NoError.into();
233            assert_ne!(wire, 0x0100, "should emit GREASE, not literal NoError");
234            assert_eq!(
235                (wire - 0x21) % 0x1f,
236                0,
237                "{wire:#x} is not a valid GREASE value"
238            );
239        }
240    }
241
242    #[test]
243    fn grease_decodes_as_no_error() {
244        for n in [0u64, 1, 100, 0xFFFF] {
245            let grease = 0x1f * n + 0x21;
246            assert_eq!(H3ErrorCode::from(grease), H3ErrorCode::NoError);
247        }
248    }
249
250    #[test]
251    fn unknown_non_grease_decodes_as_no_error() {
252        assert_eq!(H3ErrorCode::from(0xDEAD), H3ErrorCode::NoError);
253        assert_eq!(H3ErrorCode::from(0), H3ErrorCode::NoError);
254        assert_eq!(H3ErrorCode::from(u64::MAX), H3ErrorCode::NoError);
255    }
256}