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}