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