Skip to main content

trillium_http/h3/
error.rs

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