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 its major version is not 1
48 /// (`HTTP/1.2` to `HTTP/1.9` are accepted and read as HTTP/1.1). Answer
49 /// 505.
50 UnsupportedVersion,
51 /// The connection starts with the HTTP/2 connection preface
52 /// (`PRI * HTTP/2.0`, RFC 9113 section 3.4): a client speaking HTTP/2
53 /// with prior knowledge. Route the connection to an HTTP/2 engine if
54 /// the listener allows prior knowledge; otherwise answer 505 and close.
55 Http2Preface,
56 /// A received three-character status code that is not digits or is
57 /// outside `100..=599` (a code of another length is
58 /// [`InvalidStartLine`](HttpError::InvalidStartLine)); or a code the call
59 /// cannot send: outside `100..=599`, a 1xx other than 101 as a final
60 /// response, or a non-1xx (or 100 or 101) as an informational response.
61 InvalidStatus,
62 /// The request line (or status line) exceeds
63 /// [`Limits::max_start_line`](crate::Limits::max_start_line). Answer 414.
64 StartLineTooLong,
65 /// The header section exceeds
66 /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes), or a
67 /// trailer section exceeds
68 /// [`Limits::max_trailer_bytes`](crate::Limits::max_trailer_bytes); on the
69 /// client also interim heads that together exceed `max_header_bytes`.
70 /// Answer 431.
71 HeadersTooLarge,
72 /// More header fields than
73 /// [`Limits::max_headers`](crate::Limits::max_headers), or more trailer
74 /// fields than [`Limits::max_trailer_fields`](crate::Limits::max_trailer_fields).
75 /// Answer 431.
76 TooManyHeaders,
77 /// One field line is longer than
78 /// [`Limits::max_field_line`](crate::Limits::max_field_line). Answer 431.
79 FieldLineTooLong,
80 /// A carriage return that is not followed by a line feed in a received
81 /// head or trailer section, or any carriage return in an outgoing field
82 /// value (in a chunk line it is
83 /// [`InvalidChunkSize`](Self::InvalidChunkSize) or
84 /// [`InvalidChunkExtension`](Self::InvalidChunkExtension)). Refused
85 /// because recipients disagree on what it means, which is a smuggling
86 /// vector.
87 BareCr,
88 /// A line feed not preceded by a carriage return. RFC 9112 permits
89 /// accepting it; this crate refuses it so that every hop agrees on where
90 /// lines end.
91 BareLf,
92 /// A field line begins with whitespace: obsolete line folding, or
93 /// whitespace between the start line and the first field.
94 ObsFold,
95 /// A field name is empty, contains a non-token character, or the line
96 /// has no colon.
97 InvalidHeaderName,
98 /// Whitespace between a field name and its colon (RFC 9112 section 5.1
99 /// requires rejecting it).
100 WhitespaceBeforeColon,
101 /// A field value contains a control character other than horizontal tab
102 /// (a carriage return is [`BareCr`](HttpError::BareCr)); an outgoing
103 /// value has leading or trailing whitespace; or an upgrade protocol is
104 /// not a token.
105 InvalidHeaderValue,
106 /// An HTTP/1.1 request without a `Host` field.
107 MissingHost,
108 /// More than one `Host` field, or a `Host` value that is not
109 /// `host [":" port]`: also an empty host before a port (`:80`) and a
110 /// `%` not followed by two hex digits.
111 InvalidHost,
112 /// `Content-Length` is not a plain decimal number that fits in 64 bits
113 /// (signs, lists, spaces, and empty values are all refused).
114 InvalidContentLength,
115 /// `Content-Length` appears more than once. Refused even when the values
116 /// agree, so no intermediary can pick a different one.
117 DuplicateContentLength,
118 /// Both `Content-Length` and `Transfer-Encoding` are present: the classic
119 /// request smuggling vector, refused outright.
120 ConflictingFraming,
121 /// `Transfer-Encoding` is empty, repeats `chunked`, or does not end in
122 /// `chunked`.
123 InvalidTransferEncoding,
124 /// A transfer coding before the final `chunked` (for example
125 /// `gzip, chunked`). Answer 501. A `Transfer-Encoding` that does not end
126 /// in `chunked` is [`InvalidTransferEncoding`](HttpError::InvalidTransferEncoding).
127 UnsupportedTransferEncoding,
128 /// `Transfer-Encoding` in an HTTP/1.0 message, whose framing is then
129 /// faulty by RFC 9112 section 6.1.
130 TransferEncodingInHttp10,
131 /// A chunk-size line is not hexadecimal, has more than 16 hex digits
132 /// (even when the value fits, as in `00000000000000001`), or overflows
133 /// 64 bits.
134 InvalidChunkSize,
135 /// A chunk extension does not follow the `;name[=value]` grammar.
136 InvalidChunkExtension,
137 /// Chunk data is not followed by CRLF.
138 InvalidChunkTerminator,
139 /// A chunk-size line is longer than
140 /// [`Limits::max_start_line`](crate::Limits::max_start_line).
141 ChunkLineTooLong,
142 /// The chunk extensions of one message add up to more than
143 /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes). Each
144 /// line is short, but without a total a peer could make a small body
145 /// cost an unbounded amount of input (the CVE-2024-22019 pattern).
146 ChunkExtensionsTooLarge,
147 /// The body exceeds [`Limits::max_body`](crate::Limits::max_body): a
148 /// `Content-Length` or chunk size is checked before its data is buffered;
149 /// a read-until-close body is refused at the first byte past the budget.
150 /// Answer 413.
151 BodyTooLarge,
152 /// The peer closed the connection in the middle of a message.
153 UnexpectedEof,
154 /// Bytes arrived when no message was expected: a response with no
155 /// request outstanding, or data after the connection finished.
156 UnexpectedData,
157 /// A `101 Switching Protocols` to a request that did not ask to switch,
158 /// one with no `Upgrade`, or one naming a protocol the request did not
159 /// offer.
160 UnexpectedUpgrade,
161 /// The client received more interim (1xx) responses to one request
162 /// than [`Limits::max_interim`](crate::Limits::max_interim): a server
163 /// that never sends its final response. Close the connection.
164 TooManyInterim,
165 /// An outgoing message tried to set a header the connection owns
166 /// (`content-length`, `transfer-encoding`, `connection`, `upgrade`,
167 /// `trailer`, `keep-alive`, `proxy-connection`, and `te` on a response);
168 /// use the builder methods instead.
169 ReservedHeader,
170 /// A body was given for a status that cannot carry one (1xx, 204, 304,
171 /// or a 2xx to `CONNECT`).
172 BodyNotAllowed,
173 /// A streamed body's length differs from the `content_length` declared
174 /// for it.
175 BodyLengthMismatch,
176 /// A `101 Switching Protocols` names a protocol the request's `Upgrade`
177 /// field did not offer (RFC 9110 section 7.8: a server MUST NOT switch
178 /// to it), or names `h2c`, whose upgrade RFC 9113 section 3.1
179 /// deprecates. Also raised by `Request::upgrade("h2c")`. Answer with a
180 /// protocol the client offered, or with a normal response.
181 UpgradeNotOffered,
182 /// An interim (1xx) response the connection may not send now: to an
183 /// HTTP/1.0 request (RFC 9110 section 15.2 forbids it), after the final
184 /// response started, or a `100 Continue` after the body began or twice.
185 /// Skip the interim response.
186 InterimNotAllowed,
187 /// An outgoing trailer field that RFC 9110 section 6.5.1 keeps out of
188 /// trailers (framing, routing, authentication, request modifiers,
189 /// response control, content handling), or trailers on a body that is
190 /// not chunked. Send the field in the header section instead.
191 TrailerNotAllowed,
192 /// A call made in the wrong state, for example `respond` twice for one
193 /// request. The text names the rule that was broken.
194 InvalidState(&'static str),
195}
196
197impl HttpError {
198 /// The status code a server should answer with when this error came from
199 /// parsing a request, or `None` for errors that are not about a request
200 /// (response parsing, builder misuse, connection state).
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// use http_net::HttpError;
206 ///
207 /// assert_eq!(HttpError::BodyTooLarge.status(), Some(413));
208 /// assert_eq!(HttpError::TooManyHeaders.status(), Some(431));
209 /// assert_eq!(HttpError::InvalidState("x").status(), None);
210 /// ```
211 #[must_use]
212 pub const fn status(&self) -> Option<u16> {
213 match self {
214 Self::StartLineTooLong => Some(414),
215 Self::HeadersTooLarge | Self::TooManyHeaders | Self::FieldLineTooLong => Some(431),
216 Self::BodyTooLarge => Some(413),
217 Self::UnsupportedVersion | Self::Http2Preface => Some(505),
218 Self::UnsupportedTransferEncoding => Some(501),
219 Self::InvalidStartLine
220 | Self::InvalidMethod
221 | Self::InvalidTarget
222 | Self::InvalidVersion
223 | Self::BareCr
224 | Self::BareLf
225 | Self::ObsFold
226 | Self::InvalidHeaderName
227 | Self::WhitespaceBeforeColon
228 | Self::InvalidHeaderValue
229 | Self::MissingHost
230 | Self::InvalidHost
231 | Self::InvalidContentLength
232 | Self::DuplicateContentLength
233 | Self::ConflictingFraming
234 | Self::InvalidTransferEncoding
235 | Self::TransferEncodingInHttp10
236 | Self::InvalidChunkSize
237 | Self::InvalidChunkExtension
238 | Self::InvalidChunkTerminator
239 | Self::ChunkLineTooLong
240 | Self::ChunkExtensionsTooLarge => Some(400),
241 Self::InvalidStatus
242 | Self::UnexpectedEof
243 | Self::UnexpectedData
244 | Self::UnexpectedUpgrade
245 | Self::TooManyInterim
246 | Self::ReservedHeader
247 | Self::BodyNotAllowed
248 | Self::BodyLengthMismatch
249 | Self::UpgradeNotOffered
250 | Self::InterimNotAllowed
251 | Self::TrailerNotAllowed
252 | Self::InvalidState(_) => None,
253 }
254 }
255
256 /// Whether parsing bytes from the peer can produce this error. After
257 /// such an error from `poll`, the connection is finished: send at most
258 /// one error response, then close.
259 ///
260 /// Some variants are also usage errors (for example
261 /// [`InvalidTarget`](HttpError::InvalidTarget) comes from a bad request
262 /// line or from `Request::new` with a bad target): the call that
263 /// returned the error tells which it was, since `poll` returns only
264 /// peer errors.
265 ///
266 /// # Examples
267 ///
268 /// ```
269 /// use http_net::HttpError;
270 ///
271 /// assert!(HttpError::ConflictingFraming.is_peer_error());
272 /// assert!(!HttpError::ReservedHeader.is_peer_error());
273 /// ```
274 #[must_use]
275 pub const fn is_peer_error(&self) -> bool {
276 !matches!(
277 self,
278 Self::ReservedHeader
279 | Self::BodyNotAllowed
280 | Self::BodyLengthMismatch
281 | Self::UpgradeNotOffered
282 | Self::InterimNotAllowed
283 | Self::TrailerNotAllowed
284 | Self::InvalidState(_)
285 )
286 }
287
288 /// Whether a caller mistake (builder misuse, a call in the wrong state)
289 /// can produce this error. Such an error leaves the connection usable:
290 /// nothing was written.
291 ///
292 /// # Examples
293 ///
294 /// ```
295 /// use http_net::HttpError;
296 ///
297 /// assert!(HttpError::ReservedHeader.is_usage_error());
298 /// assert!(!HttpError::BareLf.is_usage_error());
299 /// // Raised by parsing or by a builder: both.
300 /// assert!(HttpError::InvalidTarget.is_peer_error() && HttpError::InvalidTarget.is_usage_error());
301 /// ```
302 #[must_use]
303 pub const fn is_usage_error(&self) -> bool {
304 matches!(
305 self,
306 Self::InvalidMethod
307 | Self::InvalidTarget
308 | Self::InvalidStatus
309 | Self::BareCr
310 | Self::InvalidHeaderName
311 | Self::InvalidHeaderValue
312 | Self::MissingHost
313 | Self::InvalidHost
314 | Self::UnexpectedData
315 | Self::ReservedHeader
316 | Self::BodyNotAllowed
317 | Self::BodyLengthMismatch
318 | Self::UpgradeNotOffered
319 | Self::InterimNotAllowed
320 | Self::TrailerNotAllowed
321 | Self::InvalidState(_)
322 )
323 }
324}
325
326impl fmt::Display for HttpError {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 let msg = match self {
329 Self::InvalidStartLine => "malformed start line: expected three space-separated parts",
330 Self::InvalidMethod => "invalid method: must be a token",
331 Self::InvalidTarget => "invalid request target",
332 Self::InvalidVersion => "invalid HTTP version: expected HTTP/DIGIT.DIGIT",
333 Self::UnsupportedVersion => "unsupported HTTP version: only HTTP/1.0 and HTTP/1.1",
334 Self::Http2Preface => {
335 "HTTP/2 connection preface on an HTTP/1.1 connection: route it to an HTTP/2 \
336 engine or answer 505"
337 }
338 Self::InvalidStatus => "invalid status code: expected three digits in 100..=599",
339 Self::StartLineTooLong => "start line exceeds the configured limit",
340 Self::HeadersTooLarge => "header section exceeds the configured byte limit",
341 Self::TooManyHeaders => "more header fields than the configured limit",
342 Self::FieldLineTooLong => "a field line exceeds the configured limit",
343 Self::BareCr => "carriage return not followed by line feed",
344 Self::BareLf => "line feed not preceded by carriage return",
345 Self::ObsFold => "field line starts with whitespace (obsolete line folding)",
346 Self::InvalidHeaderName => "invalid field name or missing colon",
347 Self::WhitespaceBeforeColon => "whitespace between field name and colon",
348 Self::InvalidHeaderValue => {
349 "control character in field value, or surrounding whitespace in an outgoing value"
350 }
351 Self::MissingHost => "HTTP/1.1 request without a Host field",
352 Self::InvalidHost => "duplicate or malformed Host field",
353 Self::InvalidContentLength => "Content-Length is not a single decimal number",
354 Self::DuplicateContentLength => "Content-Length appears more than once",
355 Self::ConflictingFraming => "both Content-Length and Transfer-Encoding are present",
356 Self::InvalidTransferEncoding => "Transfer-Encoding must end in a single chunked",
357 Self::UnsupportedTransferEncoding => "transfer coding other than chunked",
358 Self::TransferEncodingInHttp10 => "Transfer-Encoding in an HTTP/1.0 message",
359 Self::InvalidChunkSize => "invalid chunk size",
360 Self::InvalidChunkExtension => "invalid chunk extension",
361 Self::InvalidChunkTerminator => "chunk data not followed by CRLF",
362 Self::ChunkLineTooLong => "chunk-size line exceeds the configured limit",
363 Self::ChunkExtensionsTooLarge => {
364 "chunk extensions exceed the header byte limit for the message"
365 }
366 Self::BodyTooLarge => "body exceeds the configured limit",
367 Self::UnexpectedEof => "connection closed in the middle of a message",
368 Self::UnexpectedData => "data received when no message was expected",
369 Self::UnexpectedUpgrade => "protocol switch that the request did not ask for",
370 Self::TooManyInterim => "more interim responses than the configured limit",
371 Self::ReservedHeader => {
372 "header is managed by the connection; use the builder method instead"
373 }
374 Self::BodyNotAllowed => "this status code cannot carry a body",
375 Self::BodyLengthMismatch => "streamed body length differs from the declared length",
376 Self::InterimNotAllowed => {
377 "interim response not allowed now (HTTP/1.0 peer, after the final response, \
378 or 100 Continue after the body began)"
379 }
380 Self::TrailerNotAllowed => {
381 "field not allowed in trailers, or trailers on a body that is not chunked"
382 }
383 Self::UpgradeNotOffered => {
384 "101 names a protocol the request did not offer (or h2c); \
385 answer with an offered protocol or a normal response"
386 }
387 Self::InvalidState(rule) => return write!(f, "invalid call order: {rule}"),
388 };
389 f.write_str(msg)
390 }
391}
392
393impl core::error::Error for HttpError {}
394
395#[cfg(test)]
396mod tests {
397 use alloc::string::ToString;
398
399 use super::*;
400
401 #[test]
402 fn test_status_mapping_covers_request_errors() {
403 assert_eq!(HttpError::StartLineTooLong.status(), Some(414));
404 assert_eq!(HttpError::UnsupportedVersion.status(), Some(505));
405 assert_eq!(HttpError::UnsupportedTransferEncoding.status(), Some(501));
406 assert_eq!(HttpError::BareCr.status(), Some(400));
407 assert_eq!(HttpError::UnexpectedEof.status(), None);
408 }
409
410 /// Every variant, so the category and text tests cannot miss one.
411 const ALL: &[HttpError] = &[
412 HttpError::InvalidStartLine,
413 HttpError::InvalidMethod,
414 HttpError::InvalidTarget,
415 HttpError::InvalidVersion,
416 HttpError::UnsupportedVersion,
417 HttpError::Http2Preface,
418 HttpError::InvalidStatus,
419 HttpError::StartLineTooLong,
420 HttpError::HeadersTooLarge,
421 HttpError::TooManyHeaders,
422 HttpError::FieldLineTooLong,
423 HttpError::BareCr,
424 HttpError::BareLf,
425 HttpError::ObsFold,
426 HttpError::InvalidHeaderName,
427 HttpError::WhitespaceBeforeColon,
428 HttpError::InvalidHeaderValue,
429 HttpError::MissingHost,
430 HttpError::InvalidHost,
431 HttpError::InvalidContentLength,
432 HttpError::DuplicateContentLength,
433 HttpError::ConflictingFraming,
434 HttpError::InvalidTransferEncoding,
435 HttpError::UnsupportedTransferEncoding,
436 HttpError::TransferEncodingInHttp10,
437 HttpError::InvalidChunkSize,
438 HttpError::InvalidChunkExtension,
439 HttpError::InvalidChunkTerminator,
440 HttpError::ChunkLineTooLong,
441 HttpError::ChunkExtensionsTooLarge,
442 HttpError::BodyTooLarge,
443 HttpError::UnexpectedEof,
444 HttpError::UnexpectedData,
445 HttpError::UnexpectedUpgrade,
446 HttpError::TooManyInterim,
447 HttpError::ReservedHeader,
448 HttpError::BodyNotAllowed,
449 HttpError::BodyLengthMismatch,
450 HttpError::UpgradeNotOffered,
451 HttpError::InterimNotAllowed,
452 HttpError::TrailerNotAllowed,
453 HttpError::InvalidState("rule"),
454 ];
455
456 #[test]
457 fn test_every_variant_has_a_category() {
458 for e in ALL {
459 assert!(e.is_peer_error() || e.is_usage_error(), "{e:?}");
460 // A usage-only error never maps to a status: it is not about a request.
461 if !e.is_peer_error() {
462 assert_eq!(e.status(), None, "{e:?}");
463 }
464 }
465 assert!(HttpError::UpgradeNotOffered.is_usage_error());
466 assert!(!HttpError::UpgradeNotOffered.is_peer_error());
467 assert!(HttpError::BareLf.is_peer_error() && !HttpError::BareLf.is_usage_error());
468 }
469
470 /// Threat T-37: error text is static and never reflects input bytes.
471 #[test]
472 fn test_display_every_variant_is_static_ascii_text() {
473 for e in ALL {
474 let text = e.to_string();
475 assert!(!text.is_empty(), "{e:?}");
476 assert!(
477 text.bytes().all(|b| b.is_ascii_graphic() || b == b' '),
478 "{e:?}: {text}"
479 );
480 // A missing line continuation leaves a run of indentation.
481 assert!(!text.contains(" "), "{e:?}: {text}");
482 assert_eq!(text.trim(), text, "{e:?}");
483 }
484 }
485
486 #[test]
487 fn test_display_is_actionable() {
488 assert_eq!(
489 HttpError::InvalidState("respond called twice").to_string(),
490 "invalid call order: respond called twice"
491 );
492 assert!(
493 HttpError::ConflictingFraming
494 .to_string()
495 .contains("Content-Length")
496 );
497 }
498}