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}