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`).
43    InvalidTarget,
44    /// The protocol version is not of the form `HTTP/DIGIT.DIGIT`.
45    InvalidVersion,
46    /// The version is well formed but not HTTP/1.0 or HTTP/1.1. Answer 505.
47    UnsupportedVersion,
48    /// A status code outside `100..=599`, or not three digits.
49    InvalidStatus,
50    /// The request line (or status line) exceeds
51    /// [`Limits::max_start_line`](crate::Limits::max_start_line). Answer 414.
52    StartLineTooLong,
53    /// The header section exceeds
54    /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes). Answer 431.
55    HeadersTooLarge,
56    /// More header (or trailer) fields than
57    /// [`Limits::max_headers`](crate::Limits::max_headers). Answer 431.
58    TooManyHeaders,
59    /// A carriage return that is not followed by a line feed. Refused because
60    /// recipients disagree on what it means, which is a smuggling vector.
61    BareCr,
62    /// A line feed not preceded by a carriage return. RFC 9112 permits
63    /// accepting it; this crate refuses it so that every hop agrees on where
64    /// lines end.
65    BareLf,
66    /// A field line begins with whitespace: obsolete line folding, or
67    /// whitespace between the start line and the first field.
68    ObsFold,
69    /// A field name is empty, contains a non-token character, or the line
70    /// has no colon.
71    InvalidHeaderName,
72    /// Whitespace between a field name and its colon (RFC 9112 section 5.1
73    /// requires rejecting it).
74    WhitespaceBeforeColon,
75    /// A field value contains a control character other than horizontal tab.
76    InvalidHeaderValue,
77    /// An HTTP/1.1 request without a `Host` field.
78    MissingHost,
79    /// More than one `Host` field, or a `Host` value that is not
80    /// `host [":" port]`.
81    InvalidHost,
82    /// `Content-Length` is not a plain decimal number that fits in 64 bits
83    /// (signs, lists, spaces, and empty values are all refused).
84    InvalidContentLength,
85    /// `Content-Length` appears more than once. Refused even when the values
86    /// agree, so no intermediary can pick a different one.
87    DuplicateContentLength,
88    /// Both `Content-Length` and `Transfer-Encoding` are present: the classic
89    /// request smuggling vector, refused outright.
90    ConflictingFraming,
91    /// `Transfer-Encoding` is empty, repeats `chunked`, or does not end in
92    /// `chunked`.
93    InvalidTransferEncoding,
94    /// A transfer coding other than `chunked` (for example `gzip`). Answer 501.
95    UnsupportedTransferEncoding,
96    /// `Transfer-Encoding` in an HTTP/1.0 message, whose framing is then
97    /// faulty by RFC 9112 section 6.1.
98    TransferEncodingInHttp10,
99    /// A chunk-size line is not hexadecimal, or overflows 64 bits.
100    InvalidChunkSize,
101    /// A chunk extension does not follow the `;name[=value]` grammar.
102    InvalidChunkExtension,
103    /// Chunk data is not followed by CRLF.
104    InvalidChunkTerminator,
105    /// A chunk-size line is longer than
106    /// [`Limits::max_start_line`](crate::Limits::max_start_line).
107    ChunkLineTooLong,
108    /// The chunk extensions of one message add up to more than
109    /// [`Limits::max_header_bytes`](crate::Limits::max_header_bytes). Each
110    /// line is short, but without a total a peer could make a small body
111    /// cost an unbounded amount of input (the CVE-2024-22019 pattern).
112    ChunkExtensionsTooLarge,
113    /// The body exceeds [`Limits::max_body`](crate::Limits::max_body), detected
114    /// from the declared length before any of it is buffered. Answer 413.
115    BodyTooLarge,
116    /// The peer closed the connection in the middle of a message.
117    UnexpectedEof,
118    /// Bytes arrived when no message was expected: a response with no
119    /// request outstanding, or data after the connection finished.
120    UnexpectedData,
121    /// A `101 Switching Protocols` (or a 2xx to `CONNECT`) arrived for a
122    /// request that did not ask to switch.
123    UnexpectedUpgrade,
124    /// An outgoing message tried to set a header the connection owns
125    /// (`content-length`, `transfer-encoding`, `connection`, `upgrade`); use
126    /// the builder methods instead.
127    ReservedHeader,
128    /// A body was given for a status that cannot carry one (1xx, 204, 304).
129    BodyNotAllowed,
130    /// A streamed body's length differs from the `content_length` declared
131    /// for it.
132    BodyLengthMismatch,
133    /// A call made in the wrong state, for example `respond` twice for one
134    /// request. The text names the rule that was broken.
135    InvalidState(&'static str),
136}
137
138impl HttpError {
139    /// The status code a server should answer with when this error came from
140    /// parsing a request, or `None` for errors that are not about a request
141    /// (response parsing, builder misuse, connection state).
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use http_net::HttpError;
147    ///
148    /// assert_eq!(HttpError::BodyTooLarge.status(), Some(413));
149    /// assert_eq!(HttpError::TooManyHeaders.status(), Some(431));
150    /// assert_eq!(HttpError::InvalidState("x").status(), None);
151    /// ```
152    #[must_use]
153    pub const fn status(&self) -> Option<u16> {
154        match self {
155            Self::StartLineTooLong => Some(414),
156            Self::HeadersTooLarge | Self::TooManyHeaders => Some(431),
157            Self::BodyTooLarge => Some(413),
158            Self::UnsupportedVersion => Some(505),
159            Self::UnsupportedTransferEncoding => Some(501),
160            Self::InvalidStartLine
161            | Self::InvalidMethod
162            | Self::InvalidTarget
163            | Self::InvalidVersion
164            | Self::BareCr
165            | Self::BareLf
166            | Self::ObsFold
167            | Self::InvalidHeaderName
168            | Self::WhitespaceBeforeColon
169            | Self::InvalidHeaderValue
170            | Self::MissingHost
171            | Self::InvalidHost
172            | Self::InvalidContentLength
173            | Self::DuplicateContentLength
174            | Self::ConflictingFraming
175            | Self::InvalidTransferEncoding
176            | Self::TransferEncodingInHttp10
177            | Self::InvalidChunkSize
178            | Self::InvalidChunkExtension
179            | Self::InvalidChunkTerminator
180            | Self::ChunkLineTooLong
181            | Self::ChunkExtensionsTooLarge => Some(400),
182            Self::InvalidStatus
183            | Self::UnexpectedEof
184            | Self::UnexpectedData
185            | Self::UnexpectedUpgrade
186            | Self::ReservedHeader
187            | Self::BodyNotAllowed
188            | Self::BodyLengthMismatch
189            | Self::InvalidState(_) => None,
190        }
191    }
192}
193
194impl fmt::Display for HttpError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        let msg = match self {
197            Self::InvalidStartLine => "malformed start line: expected three space-separated parts",
198            Self::InvalidMethod => "invalid method: must be a token",
199            Self::InvalidTarget => "invalid request target",
200            Self::InvalidVersion => "invalid HTTP version: expected HTTP/DIGIT.DIGIT",
201            Self::UnsupportedVersion => "unsupported HTTP version: only HTTP/1.0 and HTTP/1.1",
202            Self::InvalidStatus => "invalid status code: expected three digits in 100..=599",
203            Self::StartLineTooLong => "start line exceeds the configured limit",
204            Self::HeadersTooLarge => "header section exceeds the configured byte limit",
205            Self::TooManyHeaders => "more header fields than the configured limit",
206            Self::BareCr => "carriage return not followed by line feed",
207            Self::BareLf => "line feed not preceded by carriage return",
208            Self::ObsFold => "field line starts with whitespace (obsolete line folding)",
209            Self::InvalidHeaderName => "invalid field name or missing colon",
210            Self::WhitespaceBeforeColon => "whitespace between field name and colon",
211            Self::InvalidHeaderValue => "control character in field value",
212            Self::MissingHost => "HTTP/1.1 request without a Host field",
213            Self::InvalidHost => "duplicate or malformed Host field",
214            Self::InvalidContentLength => "Content-Length is not a single decimal number",
215            Self::DuplicateContentLength => "Content-Length appears more than once",
216            Self::ConflictingFraming => "both Content-Length and Transfer-Encoding are present",
217            Self::InvalidTransferEncoding => "Transfer-Encoding must end in a single chunked",
218            Self::UnsupportedTransferEncoding => "transfer coding other than chunked",
219            Self::TransferEncodingInHttp10 => "Transfer-Encoding in an HTTP/1.0 message",
220            Self::InvalidChunkSize => "invalid chunk size",
221            Self::InvalidChunkExtension => "invalid chunk extension",
222            Self::InvalidChunkTerminator => "chunk data not followed by CRLF",
223            Self::ChunkLineTooLong => "chunk-size line exceeds the configured limit",
224            Self::ChunkExtensionsTooLarge => {
225                "chunk extensions exceed the header byte limit for the message"
226            }
227            Self::BodyTooLarge => "body exceeds the configured limit",
228            Self::UnexpectedEof => "connection closed in the middle of a message",
229            Self::UnexpectedData => "data received when no message was expected",
230            Self::UnexpectedUpgrade => "protocol switch that the request did not ask for",
231            Self::ReservedHeader => {
232                "header is managed by the connection; use the builder method instead"
233            }
234            Self::BodyNotAllowed => "this status code cannot carry a body",
235            Self::BodyLengthMismatch => "streamed body length differs from the declared length",
236            Self::InvalidState(rule) => return write!(f, "invalid call order: {rule}"),
237        };
238        f.write_str(msg)
239    }
240}
241
242impl core::error::Error for HttpError {}
243
244#[cfg(test)]
245mod tests {
246    use alloc::string::ToString;
247
248    use super::*;
249
250    #[test]
251    fn test_status_mapping_covers_request_errors() {
252        assert_eq!(HttpError::StartLineTooLong.status(), Some(414));
253        assert_eq!(HttpError::UnsupportedVersion.status(), Some(505));
254        assert_eq!(HttpError::UnsupportedTransferEncoding.status(), Some(501));
255        assert_eq!(HttpError::BareCr.status(), Some(400));
256        assert_eq!(HttpError::UnexpectedEof.status(), None);
257    }
258
259    #[test]
260    fn test_display_is_actionable() {
261        assert_eq!(
262            HttpError::InvalidState("respond called twice").to_string(),
263            "invalid call order: respond called twice"
264        );
265        assert!(
266            HttpError::ConflictingFraming
267                .to_string()
268                .contains("Content-Length")
269        );
270    }
271}