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