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    /// 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}