Skip to main content

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