http_net/error.rs
1//! The crate-wide error type.
2
3use core::fmt;
4
5/// Why a message was refused, or why a call was out of order.
6///
7/// Parse errors are final for the connection: HTTP/1.1 framing cannot be
8/// resynchronised after a malformed message without risking request
9/// smuggling, so after any parse error the connection must be closed. A
10/// server should first answer with [`status`](HttpError::status) (for
11/// example through [`ServerConn::respond`](crate::ServerConn::respond),
12/// which still accepts one response after a parse error and marks it
13/// `connection: close`).
14///
15/// Builder errors ([`ReservedHeader`](HttpError::ReservedHeader),
16/// [`InvalidHeaderName`](HttpError::InvalidHeaderName) on an outgoing message,
17/// and so on) and [`InvalidState`](HttpError::InvalidState) are programming
18/// errors on the caller's side and leave the connection usable.
19///
20/// # Examples
21///
22/// ```
23/// use http_net::{HttpError, ServerConn, ServerEvent};
24///
25/// let mut conn = ServerConn::new();
26/// let n = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n");
27/// assert!(n > 0);
28/// let err = conn.poll().unwrap_err();
29/// assert_eq!(err, HttpError::ConflictingFraming);
30/// assert_eq!(err.status(), Some(400));
31/// ```
32#[non_exhaustive]
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum HttpError {
35 /// The request line or status line does not have the shape
36 /// `method SP target SP version` / `version SP code SP reason`.
37 InvalidStartLine,
38 /// The method is not a token.
39 InvalidMethod,
40 /// The request target contains characters outside RFC 3986, a bad
41 /// percent-encoding, or a form the method does not allow (`*` outside
42 /// `OPTIONS`, authority-form outside `CONNECT`); or an absolute form
43 /// without `//authority`, with userinfo, or with an empty host.
44 InvalidTarget,
45 /// The protocol version is not of the form `HTTP/DIGIT.DIGIT`.
46 InvalidVersion,
47 /// The version is well formed but not HTTP/1.0 or HTTP/1.1. Answer 505.
48 UnsupportedVersion,
49 /// A status code outside `100..=599`, or not three digits.
50 InvalidStatus,
51 /// The request line (or status line) exceeds
52 /// [`Limits::max_start_line`](crate::Limits::max_start_line). Answer 414.
53 StartLineTooLong,
54 /// The header section exceeds
55 /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes). Answer 431.
56 HeadersTooLarge,
57 /// More header (or trailer) fields than
58 /// [`Limits::max_headers`](crate::Limits::max_headers). Answer 431.
59 TooManyHeaders,
60 /// A carriage return that is not followed by a line feed. Refused because
61 /// recipients disagree on what it means, which is a smuggling vector.
62 BareCr,
63 /// A line feed not preceded by a carriage return. RFC 9112 permits
64 /// accepting it; this crate refuses it so that every hop agrees on where
65 /// lines end.
66 BareLf,
67 /// A field line begins with whitespace: obsolete line folding, or
68 /// whitespace between the start line and the first field.
69 ObsFold,
70 /// A field name is empty, contains a non-token character, or the line
71 /// has no colon.
72 InvalidHeaderName,
73 /// Whitespace between a field name and its colon (RFC 9112 section 5.1
74 /// requires rejecting it).
75 WhitespaceBeforeColon,
76 /// A field value contains a control character other than horizontal tab.
77 InvalidHeaderValue,
78 /// An HTTP/1.1 request without a `Host` field.
79 MissingHost,
80 /// More than one `Host` field, or a `Host` value that is not
81 /// `host [":" port]`: also an empty host before a port (`:80`) and a
82 /// `%` not followed by two hex digits.
83 InvalidHost,
84 /// `Content-Length` is not a plain decimal number that fits in 64 bits
85 /// (signs, lists, spaces, and empty values are all refused).
86 InvalidContentLength,
87 /// `Content-Length` appears more than once. Refused even when the values
88 /// agree, so no intermediary can pick a different one.
89 DuplicateContentLength,
90 /// Both `Content-Length` and `Transfer-Encoding` are present: the classic
91 /// request smuggling vector, refused outright.
92 ConflictingFraming,
93 /// `Transfer-Encoding` is empty, repeats `chunked`, or does not end in
94 /// `chunked`.
95 InvalidTransferEncoding,
96 /// A transfer coding other than `chunked` (for example `gzip`). Answer 501.
97 UnsupportedTransferEncoding,
98 /// `Transfer-Encoding` in an HTTP/1.0 message, whose framing is then
99 /// faulty by RFC 9112 section 6.1.
100 TransferEncodingInHttp10,
101 /// A chunk-size line is not hexadecimal, or overflows 64 bits.
102 InvalidChunkSize,
103 /// A chunk extension does not follow the `;name[=value]` grammar.
104 InvalidChunkExtension,
105 /// Chunk data is not followed by CRLF.
106 InvalidChunkTerminator,
107 /// A chunk-size line is longer than
108 /// [`Limits::max_start_line`](crate::Limits::max_start_line).
109 ChunkLineTooLong,
110 /// The chunk extensions of one message add up to more than
111 /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes). Each
112 /// line is short, but without a total a peer could make a small body
113 /// cost an unbounded amount of input (the CVE-2024-22019 pattern).
114 ChunkExtensionsTooLarge,
115 /// The body exceeds [`Limits::max_body`](crate::Limits::max_body), detected
116 /// from the declared length before any of it is buffered. Answer 413.
117 BodyTooLarge,
118 /// The peer closed the connection in the middle of a message.
119 UnexpectedEof,
120 /// Bytes arrived when no message was expected: a response with no
121 /// request outstanding, or data after the connection finished.
122 UnexpectedData,
123 /// A `101 Switching Protocols` (or a 2xx to `CONNECT`) arrived for a
124 /// request that did not ask to switch.
125 UnexpectedUpgrade,
126 /// An outgoing message tried to set a header the connection owns
127 /// (`content-length`, `transfer-encoding`, `connection`, `upgrade`); use
128 /// the builder methods instead.
129 ReservedHeader,
130 /// A body was given for a status that cannot carry one (1xx, 204, 304).
131 BodyNotAllowed,
132 /// A streamed body's length differs from the `content_length` declared
133 /// for it.
134 BodyLengthMismatch,
135 /// A `101 Switching Protocols` names a protocol the request's `Upgrade`
136 /// field did not offer (RFC 9110 section 7.8: a server MUST NOT switch
137 /// to it), or names `h2c`, whose upgrade RFC 9113 section 3.1
138 /// deprecates. Also raised by `Request::upgrade("h2c")`. Answer with a
139 /// protocol the client offered, or with a normal response.
140 UpgradeNotOffered,
141 /// A call made in the wrong state, for example `respond` twice for one
142 /// request. The text names the rule that was broken.
143 InvalidState(&'static str),
144}
145
146impl HttpError {
147 /// The status code a server should answer with when this error came from
148 /// parsing a request, or `None` for errors that are not about a request
149 /// (response parsing, builder misuse, connection state).
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use http_net::HttpError;
155 ///
156 /// assert_eq!(HttpError::BodyTooLarge.status(), Some(413));
157 /// assert_eq!(HttpError::TooManyHeaders.status(), Some(431));
158 /// assert_eq!(HttpError::InvalidState("x").status(), None);
159 /// ```
160 #[must_use]
161 pub const fn status(&self) -> Option<u16> {
162 match self {
163 Self::StartLineTooLong => Some(414),
164 Self::HeadersTooLarge | Self::TooManyHeaders => Some(431),
165 Self::BodyTooLarge => Some(413),
166 Self::UnsupportedVersion => Some(505),
167 Self::UnsupportedTransferEncoding => Some(501),
168 Self::InvalidStartLine
169 | Self::InvalidMethod
170 | Self::InvalidTarget
171 | Self::InvalidVersion
172 | Self::BareCr
173 | Self::BareLf
174 | Self::ObsFold
175 | Self::InvalidHeaderName
176 | Self::WhitespaceBeforeColon
177 | Self::InvalidHeaderValue
178 | Self::MissingHost
179 | Self::InvalidHost
180 | Self::InvalidContentLength
181 | Self::DuplicateContentLength
182 | Self::ConflictingFraming
183 | Self::InvalidTransferEncoding
184 | Self::TransferEncodingInHttp10
185 | Self::InvalidChunkSize
186 | Self::InvalidChunkExtension
187 | Self::InvalidChunkTerminator
188 | Self::ChunkLineTooLong
189 | Self::ChunkExtensionsTooLarge => Some(400),
190 Self::InvalidStatus
191 | Self::UnexpectedEof
192 | Self::UnexpectedData
193 | Self::UnexpectedUpgrade
194 | Self::ReservedHeader
195 | Self::BodyNotAllowed
196 | Self::BodyLengthMismatch
197 | Self::UpgradeNotOffered
198 | Self::InvalidState(_) => None,
199 }
200 }
201
202 /// Whether parsing bytes from the peer can produce this error. After
203 /// such an error from `poll`, the connection is finished: send at most
204 /// one error response, then close.
205 ///
206 /// Some variants are also usage errors (for example
207 /// [`InvalidTarget`](HttpError::InvalidTarget) comes from a bad request
208 /// line or from `Request::new` with a bad target): the call that
209 /// returned the error tells which it was, since `poll` returns only
210 /// peer errors.
211 ///
212 /// # Examples
213 ///
214 /// ```
215 /// use http_net::HttpError;
216 ///
217 /// assert!(HttpError::ConflictingFraming.is_peer_error());
218 /// assert!(!HttpError::ReservedHeader.is_peer_error());
219 /// ```
220 #[must_use]
221 pub const fn is_peer_error(&self) -> bool {
222 !matches!(
223 self,
224 Self::ReservedHeader
225 | Self::BodyNotAllowed
226 | Self::BodyLengthMismatch
227 | Self::UpgradeNotOffered
228 | Self::InvalidState(_)
229 )
230 }
231
232 /// Whether a caller mistake (builder misuse, a call in the wrong state)
233 /// can produce this error. Such an error leaves the connection usable:
234 /// nothing was written.
235 ///
236 /// # Examples
237 ///
238 /// ```
239 /// use http_net::HttpError;
240 ///
241 /// assert!(HttpError::ReservedHeader.is_usage_error());
242 /// assert!(!HttpError::BareLf.is_usage_error());
243 /// // Raised by parsing or by a builder: both.
244 /// assert!(HttpError::InvalidTarget.is_peer_error() && HttpError::InvalidTarget.is_usage_error());
245 /// ```
246 #[must_use]
247 pub const fn is_usage_error(&self) -> bool {
248 matches!(
249 self,
250 Self::InvalidMethod
251 | Self::InvalidTarget
252 | Self::InvalidStatus
253 | Self::BareCr
254 | Self::InvalidHeaderName
255 | Self::InvalidHeaderValue
256 | Self::MissingHost
257 | Self::InvalidHost
258 | Self::UnexpectedData
259 | Self::ReservedHeader
260 | Self::BodyNotAllowed
261 | Self::BodyLengthMismatch
262 | Self::UpgradeNotOffered
263 | Self::InvalidState(_)
264 )
265 }
266}
267
268impl fmt::Display for HttpError {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 let msg = match self {
271 Self::InvalidStartLine => "malformed start line: expected three space-separated parts",
272 Self::InvalidMethod => "invalid method: must be a token",
273 Self::InvalidTarget => "invalid request target",
274 Self::InvalidVersion => "invalid HTTP version: expected HTTP/DIGIT.DIGIT",
275 Self::UnsupportedVersion => "unsupported HTTP version: only HTTP/1.0 and HTTP/1.1",
276 Self::InvalidStatus => "invalid status code: expected three digits in 100..=599",
277 Self::StartLineTooLong => "start line exceeds the configured limit",
278 Self::HeadersTooLarge => "header section exceeds the configured byte limit",
279 Self::TooManyHeaders => "more header fields than the configured limit",
280 Self::BareCr => "carriage return not followed by line feed",
281 Self::BareLf => "line feed not preceded by carriage return",
282 Self::ObsFold => "field line starts with whitespace (obsolete line folding)",
283 Self::InvalidHeaderName => "invalid field name or missing colon",
284 Self::WhitespaceBeforeColon => "whitespace between field name and colon",
285 Self::InvalidHeaderValue => "control character in field value",
286 Self::MissingHost => "HTTP/1.1 request without a Host field",
287 Self::InvalidHost => "duplicate or malformed Host field",
288 Self::InvalidContentLength => "Content-Length is not a single decimal number",
289 Self::DuplicateContentLength => "Content-Length appears more than once",
290 Self::ConflictingFraming => "both Content-Length and Transfer-Encoding are present",
291 Self::InvalidTransferEncoding => "Transfer-Encoding must end in a single chunked",
292 Self::UnsupportedTransferEncoding => "transfer coding other than chunked",
293 Self::TransferEncodingInHttp10 => "Transfer-Encoding in an HTTP/1.0 message",
294 Self::InvalidChunkSize => "invalid chunk size",
295 Self::InvalidChunkExtension => "invalid chunk extension",
296 Self::InvalidChunkTerminator => "chunk data not followed by CRLF",
297 Self::ChunkLineTooLong => "chunk-size line exceeds the configured limit",
298 Self::ChunkExtensionsTooLarge => {
299 "chunk extensions exceed the header byte limit for the message"
300 }
301 Self::BodyTooLarge => "body exceeds the configured limit",
302 Self::UnexpectedEof => "connection closed in the middle of a message",
303 Self::UnexpectedData => "data received when no message was expected",
304 Self::UnexpectedUpgrade => "protocol switch that the request did not ask for",
305 Self::ReservedHeader => {
306 "header is managed by the connection; use the builder method instead"
307 }
308 Self::BodyNotAllowed => "this status code cannot carry a body",
309 Self::BodyLengthMismatch => "streamed body length differs from the declared length",
310 Self::UpgradeNotOffered => {
311 "101 names a protocol the request did not offer (or h2c); \
312 answer with an offered protocol or a normal response"
313 }
314 Self::InvalidState(rule) => return write!(f, "invalid call order: {rule}"),
315 };
316 f.write_str(msg)
317 }
318}
319
320impl core::error::Error for HttpError {}
321
322#[cfg(test)]
323mod tests {
324 use alloc::string::ToString;
325
326 use super::*;
327
328 #[test]
329 fn test_status_mapping_covers_request_errors() {
330 assert_eq!(HttpError::StartLineTooLong.status(), Some(414));
331 assert_eq!(HttpError::UnsupportedVersion.status(), Some(505));
332 assert_eq!(HttpError::UnsupportedTransferEncoding.status(), Some(501));
333 assert_eq!(HttpError::BareCr.status(), Some(400));
334 assert_eq!(HttpError::UnexpectedEof.status(), None);
335 }
336
337 /// Every variant, so the category and text tests cannot miss one.
338 const ALL: &[HttpError] = &[
339 HttpError::InvalidStartLine,
340 HttpError::InvalidMethod,
341 HttpError::InvalidTarget,
342 HttpError::InvalidVersion,
343 HttpError::UnsupportedVersion,
344 HttpError::InvalidStatus,
345 HttpError::StartLineTooLong,
346 HttpError::HeadersTooLarge,
347 HttpError::TooManyHeaders,
348 HttpError::BareCr,
349 HttpError::BareLf,
350 HttpError::ObsFold,
351 HttpError::InvalidHeaderName,
352 HttpError::WhitespaceBeforeColon,
353 HttpError::InvalidHeaderValue,
354 HttpError::MissingHost,
355 HttpError::InvalidHost,
356 HttpError::InvalidContentLength,
357 HttpError::DuplicateContentLength,
358 HttpError::ConflictingFraming,
359 HttpError::InvalidTransferEncoding,
360 HttpError::UnsupportedTransferEncoding,
361 HttpError::TransferEncodingInHttp10,
362 HttpError::InvalidChunkSize,
363 HttpError::InvalidChunkExtension,
364 HttpError::InvalidChunkTerminator,
365 HttpError::ChunkLineTooLong,
366 HttpError::ChunkExtensionsTooLarge,
367 HttpError::BodyTooLarge,
368 HttpError::UnexpectedEof,
369 HttpError::UnexpectedData,
370 HttpError::UnexpectedUpgrade,
371 HttpError::ReservedHeader,
372 HttpError::BodyNotAllowed,
373 HttpError::BodyLengthMismatch,
374 HttpError::UpgradeNotOffered,
375 HttpError::InvalidState("rule"),
376 ];
377
378 #[test]
379 fn test_every_variant_has_a_category() {
380 for e in ALL {
381 assert!(e.is_peer_error() || e.is_usage_error(), "{e:?}");
382 // A usage-only error never maps to a status: it is not about a request.
383 if !e.is_peer_error() {
384 assert_eq!(e.status(), None, "{e:?}");
385 }
386 }
387 assert!(HttpError::UpgradeNotOffered.is_usage_error());
388 assert!(!HttpError::UpgradeNotOffered.is_peer_error());
389 assert!(HttpError::BareLf.is_peer_error() && !HttpError::BareLf.is_usage_error());
390 }
391
392 /// Threat T-37: error text is static and never reflects input bytes.
393 #[test]
394 fn test_display_every_variant_is_static_ascii_text() {
395 for e in ALL {
396 let text = e.to_string();
397 assert!(!text.is_empty(), "{e:?}");
398 assert!(
399 text.bytes().all(|b| b.is_ascii_graphic() || b == b' '),
400 "{e:?}: {text}"
401 );
402 }
403 }
404
405 #[test]
406 fn test_display_is_actionable() {
407 assert_eq!(
408 HttpError::InvalidState("respond called twice").to_string(),
409 "invalid call order: respond called twice"
410 );
411 assert!(
412 HttpError::ConflictingFraming
413 .to_string()
414 .contains("Content-Length")
415 );
416 }
417}