netune_http1/error.rs
1//! Typed failures. The codec never panics on malformed input: every rejection is
2//! one of these, so a hostile or broken peer cannot take the process down.
3
4use std::fmt;
5
6/// A protocol, limit or transport failure while reading a response.
7#[derive(Debug)]
8pub enum HttpError {
9 /// The peer's bytes are not valid HTTP/1.x.
10 Protocol(&'static str),
11 /// A configured limit was exceeded. The peer is either broken or hostile.
12 LimitExceeded(&'static str),
13 /// The peer's stream ended before the message was complete.
14 ///
15 /// Distinct from [`HttpError::Protocol`] because the peer did not speak
16 /// broken HTTP — it stopped speaking. That is a transient condition at the
17 /// connection level (the socket died, an idle-culled keep-alive was reused,
18 /// a gateway cut a long stream) rather than a judgement about the peer's
19 /// syntax, and the two demand opposite retry verdicts. Folding them together
20 /// made one physical event — a peer closing a socket — retryable or terminal
21 /// depending on how politely it closed.
22 Incomplete(&'static str),
23 /// The transport failed.
24 Io(std::io::Error),
25}
26
27impl HttpError {
28 /// A stable, greppable classification for retry decisions.
29 pub const fn class(&self) -> &'static str {
30 match self {
31 Self::Protocol(_) => "protocol",
32 Self::LimitExceeded(_) => "limit",
33 Self::Incomplete(_) => "incomplete",
34 Self::Io(_) => "io",
35 }
36 }
37
38 /// Whether a retry could plausibly succeed.
39 ///
40 /// Transport failures and premature end-of-stream qualify: a fresh
41 /// connection is a different connection, so the same request may well be
42 /// answered. A peer speaking broken HTTP will speak it again, a limit will
43 /// be exceeded again, and an unsupported encoding will stay unsupported.
44 pub const fn is_retryable(&self) -> bool {
45 matches!(self, Self::Io(_) | Self::Incomplete(_))
46 }
47}
48
49impl fmt::Display for HttpError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::Protocol(what) => write!(f, "invalid HTTP response: {what}"),
53 Self::LimitExceeded(what) => write!(f, "HTTP response exceeded {what}"),
54 Self::Incomplete(what) => write!(
55 f,
56 "connection closed before the response was complete: {what}"
57 ),
58 Self::Io(error) => write!(f, "transport error: {error}"),
59 }
60 }
61}
62
63impl std::error::Error for HttpError {
64 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65 match self {
66 Self::Io(error) => Some(error),
67 _ => None,
68 }
69 }
70}
71
72impl From<std::io::Error> for HttpError {
73 fn from(error: std::io::Error) -> Self {
74 Self::Io(error)
75 }
76}