Skip to main content

vibeio_http/h3/
error.rs

1//! HTTP/3 error types and the QUIC transport error abstraction.
2//!
3//! Two families of errors exist in the HTTP/3 stack:
4//!
5//! - [`TransportError`]: failures surfaced by the QUIC transport
6//!   abstraction ([`crate::h3::transport`]). The HTTP/3 layer translates
7//!   these into connection closes and stream resets with typed HTTP/3
8//!   codes, and into `io::Error` at the `HttpProtocol` boundary.
9//! - [`H3Error`]: the typed application errors of RFC 9114 Section 8.1,
10//!   each with its wire code. QPACK errors ([`crate::h3::qpack::QpackError`])
11//!   form the RFC 9204 Section 6 family (codes `0x200`-`0x202`) and live
12//!   with the QPACK codec; the connection driver maps both families onto
13//!   reset/shutdown codes and `io::Error`.
14//!
15//! Error codes of the format `0x1f * N + 0x21` are reserved; per RFC 9114
16//! Section 8.1 they are treated as equivalent to `H3_NO_ERROR`.
17
18use std::{error::Error, fmt, io};
19
20/// Errors surfaced by the QUIC transport abstraction
21/// ([`crate::h3::transport`]).
22///
23/// Adapters translate their stack's error types into these variants; the
24/// HTTP/3 layer never inspects stack-specific errors.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TransportError {
27    /// The connection was closed by the peer with an application error
28    /// `code` (an HTTP/3 error code, or 0 for `H3_NO_ERROR`).
29    Closed { code: u64 },
30    /// The connection was closed by transport-level events (for example a
31    /// transport error, idle timeout, or a local close) without an
32    /// application error code.
33    Transport,
34    /// The connection timed out.
35    Timeout,
36    /// The peer reset the stream (`RESET_STREAM`) with the given error
37    /// `code`; the read side of the stream is terminated.
38    Reset { code: u64 },
39    /// The peer sent `STOP_SENDING` with the given error `code`; the write
40    /// side of the stream is terminated.
41    Stopped { code: u64 },
42    /// Any other transport-level failure.
43    Other,
44}
45
46impl fmt::Display for TransportError {
47    #[inline]
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            TransportError::Closed { code } => write!(f, "connection closed with code {code:#x}"),
51            TransportError::Transport => write!(f, "transport closed the connection"),
52            TransportError::Timeout => write!(f, "transport timed out"),
53            TransportError::Reset { code } => write!(f, "stream reset with code {code:#x}"),
54            TransportError::Stopped { code } => write!(f, "stream stopped with code {code:#x}"),
55            TransportError::Other => write!(f, "transport error"),
56        }
57    }
58}
59
60impl Error for TransportError {}
61
62impl From<TransportError> for io::Error {
63    #[inline]
64    fn from(err: TransportError) -> io::Error {
65        match err {
66            TransportError::Closed { .. } => io::Error::new(io::ErrorKind::ConnectionAborted, err),
67            TransportError::Reset { .. } | TransportError::Stopped { .. } => {
68                io::Error::new(io::ErrorKind::ConnectionReset, err)
69            }
70            TransportError::Timeout => io::Error::new(io::ErrorKind::TimedOut, err),
71            // Same conversion strategy as the previous `h3` wrapper: any
72            // other error surfaces as `io::Error::other`.
73            TransportError::Transport | TransportError::Other => io::Error::other(err),
74        }
75    }
76}
77
78/// Typed HTTP/3 application errors (RFC 9114 Section 8.1).
79///
80/// Each variant carries the error code it is sent as in `RESET_STREAM`,
81/// `STOP_SENDING`, and `CONNECTION_CLOSE` frames.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum H3Error {
84    /// `H3_NO_ERROR` (0x0100): no error; used to close cleanly.
85    NoError,
86    /// `H3_GENERAL_PROTOCOL_ERROR` (0x0101): a protocol violation that no
87    /// more specific code covers.
88    GeneralProtocol,
89    /// `H3_INTERNAL_ERROR` (0x0102): an internal error in the HTTP stack.
90    Internal,
91    /// `H3_STREAM_CREATION_ERROR` (0x0103): the peer created a stream this
92    /// endpoint will not accept.
93    StreamCreation,
94    /// `H3_CLOSED_CRITICAL_STREAM` (0x0104): a stream required by the
95    /// connection (control, QPACK encoder or decoder) was closed or reset.
96    ClosedCriticalStream,
97    /// `H3_FRAME_UNEXPECTED` (0x0105): a frame that is not permitted in
98    /// the current state or on the current stream.
99    FrameUnexpected,
100    /// `H3_FRAME_ERROR` (0x0106): a frame that violates layout requirements
101    /// or has an invalid size.
102    FrameError,
103    /// `H3_EXCESSIVE_LOAD` (0x0107): the peer is generating excessive load.
104    ExcessiveLoad,
105    /// `H3_ID_ERROR` (0x0108): a stream ID or push ID used incorrectly.
106    Id,
107    /// `H3_SETTINGS_ERROR` (0x0109): an error in the payload of a SETTINGS
108    /// frame.
109    Settings,
110    /// `H3_MISSING_SETTINGS` (0x010a): no SETTINGS frame at the start of
111    /// the control stream.
112    MissingSettings,
113    /// `H3_REQUEST_REJECTED` (0x010b): a server rejected a request without
114    /// application processing.
115    RequestRejected,
116    /// `H3_REQUEST_CANCELLED` (0x010c): the request or its response was
117    /// cancelled.
118    RequestCancelled,
119    /// `H3_REQUEST_INCOMPLETE` (0x010d): a stream terminated without a
120    /// fully formed request.
121    RequestIncomplete,
122    /// `H3_MESSAGE_ERROR` (0x010e): an HTTP message was malformed.
123    Message,
124    /// `H3_CONNECT_ERROR` (0x010f): the TCP connection behind a CONNECT
125    /// request was reset or abnormally closed.
126    Connect,
127    /// `H3_VERSION_FALLBACK` (0x0110): the requested operation cannot be
128    /// served over HTTP/3; the peer should retry over HTTP/1.1.
129    VersionFallback,
130}
131
132impl H3Error {
133    /// The RFC 9114 Section 8.1 error code for this error.
134    pub const fn code(self) -> u64 {
135        use H3Error::*;
136        match self {
137            NoError => 0x0100,
138            GeneralProtocol => 0x0101,
139            Internal => 0x0102,
140            StreamCreation => 0x0103,
141            ClosedCriticalStream => 0x0104,
142            FrameUnexpected => 0x0105,
143            FrameError => 0x0106,
144            ExcessiveLoad => 0x0107,
145            Id => 0x0108,
146            Settings => 0x0109,
147            MissingSettings => 0x010a,
148            RequestRejected => 0x010b,
149            RequestCancelled => 0x010c,
150            RequestIncomplete => 0x010d,
151            Message => 0x010e,
152            Connect => 0x010f,
153            VersionFallback => 0x0110,
154        }
155    }
156
157    /// Looks up a known HTTP/3 error by its RFC 9114 Section 8.1 code.
158    ///
159    /// Returns `None` for unknown codes. Per RFC 9114 Section 8.1, unknown
160    /// codes — including the reserved `0x1f * N + 0x21` family — are
161    /// treated as equivalent to [`H3Error::NoError`] by the wire protocol.
162    pub const fn from_code(code: u64) -> Option<H3Error> {
163        use H3Error::*;
164        Some(match code {
165            0x0100 => NoError,
166            0x0101 => GeneralProtocol,
167            0x0102 => Internal,
168            0x0103 => StreamCreation,
169            0x0104 => ClosedCriticalStream,
170            0x0105 => FrameUnexpected,
171            0x0106 => FrameError,
172            0x0107 => ExcessiveLoad,
173            0x0108 => Id,
174            0x0109 => Settings,
175            0x010a => MissingSettings,
176            0x010b => RequestRejected,
177            0x010c => RequestCancelled,
178            0x010d => RequestIncomplete,
179            0x010e => Message,
180            0x010f => Connect,
181            0x0110 => VersionFallback,
182            _ => return None,
183        })
184    }
185}
186
187impl fmt::Display for H3Error {
188    #[inline]
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{:?} ({:#06x})", self, self.code())
191    }
192}
193
194impl Error for H3Error {}
195
196impl From<H3Error> for io::Error {
197    #[inline]
198    fn from(err: H3Error) -> io::Error {
199        io::Error::other(err)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn rfc_9114_codes_round_trip() {
209        let table = [
210            (H3Error::NoError, 0x0100),
211            (H3Error::GeneralProtocol, 0x0101),
212            (H3Error::Internal, 0x0102),
213            (H3Error::StreamCreation, 0x0103),
214            (H3Error::ClosedCriticalStream, 0x0104),
215            (H3Error::FrameUnexpected, 0x0105),
216            (H3Error::FrameError, 0x0106),
217            (H3Error::ExcessiveLoad, 0x0107),
218            (H3Error::Id, 0x0108),
219            (H3Error::Settings, 0x0109),
220            (H3Error::MissingSettings, 0x010a),
221            (H3Error::RequestRejected, 0x010b),
222            (H3Error::RequestCancelled, 0x010c),
223            (H3Error::RequestIncomplete, 0x010d),
224            (H3Error::Message, 0x010e),
225            (H3Error::Connect, 0x010f),
226            (H3Error::VersionFallback, 0x0110),
227        ];
228        for (err, code) in table {
229            assert_eq!(err.code(), code, "{err:?} code");
230            assert_eq!(H3Error::from_code(code), Some(err));
231        }
232        assert_eq!(H3Error::from_code(0x00ff), None);
233        assert_eq!(H3Error::from_code(0x0111), None);
234        assert_eq!(H3Error::from_code(0x0200), None); // QPACK family is separate
235    }
236
237    #[test]
238    fn qpack_family_is_separate() {
239        // QPACK error codes (RFC 9204 Section 6) must never collide with
240        // the HTTP/3 family, so the two families can be distinguished by
241        // code alone.
242        assert_eq!(
243            crate::h3::qpack::QpackError::DecompressionFailed.code(),
244            0x0200
245        );
246        assert_eq!(crate::h3::qpack::QpackError::EncoderStream.code(), 0x0201);
247        assert_eq!(crate::h3::qpack::QpackError::DecoderStream.code(), 0x0202);
248    }
249
250    #[test]
251    fn transport_error_to_io_kinds() {
252        let err: io::Error = TransportError::Closed { code: 0x0100 }.into();
253        assert_eq!(err.kind(), io::ErrorKind::ConnectionAborted);
254        let err: io::Error = TransportError::Reset { code: 0x010c }.into();
255        assert_eq!(err.kind(), io::ErrorKind::ConnectionReset);
256        let err: io::Error = TransportError::Stopped { code: 0x010c }.into();
257        assert_eq!(err.kind(), io::ErrorKind::ConnectionReset);
258        let err: io::Error = TransportError::Timeout.into();
259        assert_eq!(err.kind(), io::ErrorKind::TimedOut);
260        let err: io::Error = TransportError::Transport.into();
261        assert_eq!(err.kind(), io::ErrorKind::Other);
262        let err: io::Error = TransportError::Other.into();
263        assert_eq!(err.kind(), io::ErrorKind::Other);
264    }
265
266    #[test]
267    fn h3_error_to_io_mentions_code() {
268        let err: io::Error = H3Error::FrameUnexpected.into();
269        let text = err.to_string();
270        assert!(text.contains("0x0105"), "got: {text}");
271        assert!(text.contains("FrameUnexpected"), "got: {text}");
272    }
273}