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