Skip to main content

ergon_io/
error.rs

1//! Error type.
2
3use std::fmt;
4
5/// An I/O, protocol or configuration error.
6#[derive(Debug)]
7#[non_exhaustive]
8pub enum Error {
9    /// Operating-system I/O failure.
10    Io(std::io::Error),
11    /// TLS failure reported by rustls.
12    Tls(rustls::Error),
13    /// Record-layer failure (bad record, AEAD failure, unexpected content).
14    Record(&'static str),
15    /// WebSocket protocol violation.
16    Ws(&'static str),
17    /// HTTP failure (a malformed response or upgrade).
18    Http(String),
19    /// The server answered the WebSocket upgrade with `status`, not 101.
20    Upgrade {
21        /// HTTP status code.
22        status: u16,
23        /// The start of the response body (at most 300 bytes).
24        body: String,
25        /// The server's `Retry-After`, in ms (see
26        /// [`crate::outcome::retry_after_ms`]); `None` without one. Wait at
27        /// least this long before connecting again (a 429 or 418 refusal).
28        retry_after_ms: Option<u64>,
29        /// The response had a `Retry-After` that is neither seconds nor an
30        /// HTTP date (`retry_after_ms` is then `None`).
31        retry_after_invalid: bool,
32    },
33    /// An HTTP connection closed with requests unanswered. `HttpConn` users
34    /// get it in `Handler::on_closed`; `HttpClient` reports each request's
35    /// [`crate::outcome::Sent`] to `ClientHandler::on_error` instead. Boxed:
36    /// rare.
37    Unanswered(Box<Unanswered>),
38    /// The libcurl `HttpClient` refused `method`: libcurl sends a request
39    /// again, up to 5 times, when the reused connection it went out on dies
40    /// before any reply byte, so an order could run twice. Without
41    /// `HttpClientConfig::curl_allow_non_idempotent_methods` it takes only
42    /// GET, HEAD, OPTIONS and DELETE. The native client never re-sends.
43    CurlResend {
44        /// The refused method.
45        method: String,
46    },
47    /// The peer closed the connection without a WebSocket close handshake.
48    Closed,
49    /// A WebSocket close handshake ended the connection: the code and
50    /// reason, and who started it. Boxed: rare, and it keeps `Result` small.
51    WsClose(Box<crate::ws::Close>),
52    /// Bad configuration input.
53    Config(String),
54    /// The backend is not available on this platform or build.
55    Unsupported(&'static str),
56    /// Name resolution failed.
57    Dns(String),
58    /// An operation did not finish in time.
59    Timeout(&'static str),
60    /// A limit was reached; retry later.
61    Busy(&'static str),
62    /// The quick-ACK route check refused the connection
63    /// ([`crate::route::QuickAckCheck::Require`]). Boxed: it is rare, and
64    /// every `Result` of this crate carries the size of the largest variant.
65    QuickAck(Box<crate::route::QuickAckError>),
66    /// A helper thread has no CPU it may use ([`crate::placement`]): the
67    /// process may use the polling CPU only (a pod with one CPU), or a CPU
68    /// setting names one outside its cpuset. Boxed: rare.
69    Cpu(Box<crate::placement::CpuError>),
70    /// The host-check switch (`ERGON_HOST_CHECKS`): it is set to something
71    /// other than `off`, `on` or `require`, or it is `require` and a host
72    /// check warned ([`crate::checks::HostReport::refusal`]). Boxed: rare.
73    HostCheck(Box<ergon_tune::HostCheckError>),
74}
75
76/// Result alias.
77pub type Result<T> = std::result::Result<T, Error>;
78
79/// [`Error::Unanswered`]: why an HTTP connection closed with requests in
80/// flight, and which of them never reached the kernel.
81#[derive(Debug)]
82pub struct Unanswered {
83    /// Why the connection closed ([`Error::Closed`], [`Error::Timeout`],
84    /// [`Error::Io`], ...).
85    pub cause: Error,
86    /// The first unanswered request (the id `HttpConn::request` returned)
87    /// whose TLS record was not wholly handed to the kernel. It and every
88    /// later request cannot have reached the server: the server cannot
89    /// decrypt part of a record. `None`: every unanswered request was
90    /// handed, so each may have run.
91    pub first_unsent: Option<u64>,
92}
93
94impl fmt::Display for Error {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Self::Io(e) => write!(f, "io: {e}"),
98            Self::Tls(e) => write!(f, "tls: {e}"),
99            Self::Record(e) => write!(f, "tls record: {e}"),
100            Self::Ws(e) => write!(f, "websocket: {e}"),
101            Self::Http(e) => write!(f, "http: {e}"),
102            Self::Upgrade {
103                status,
104                body,
105                retry_after_ms,
106                retry_after_invalid,
107            } => {
108                write!(f, "websocket upgrade refused: status {status}")?;
109                if let Some(ms) = retry_after_ms {
110                    write!(f, ", Retry-After {ms} ms")?;
111                }
112                if *retry_after_invalid {
113                    write!(f, ", Retry-After unreadable")?;
114                }
115                write!(f, ": {body}")
116            }
117            Self::Unanswered(u) => match u.first_unsent {
118                // The cause first: callers match on its text.
119                Some(id) => write!(
120                    f,
121                    "{} (requests unanswered; request {id} and later never reached the kernel)",
122                    u.cause
123                ),
124                None => write!(
125                    f,
126                    "{} (requests unanswered, all handed to the kernel)",
127                    u.cause
128                ),
129            },
130            Self::CurlResend { method } => write!(
131                f,
132                "libcurl client refused {method}: libcurl may send it again on a reused \
133                 connection that dies before the reply; use the native client or set \
134                 curl_allow_non_idempotent_methods"
135            ),
136            Self::Closed => write!(f, "peer closed the connection"),
137            Self::WsClose(c) => write!(f, "websocket {c}"),
138            Self::Config(e) => write!(f, "config: {e}"),
139            Self::Unsupported(e) => write!(f, "unsupported: {e}"),
140            Self::Dns(e) => write!(f, "dns: {e}"),
141            Self::Timeout(e) => write!(f, "timed out: {e}"),
142            Self::Busy(e) => write!(f, "busy: {e}"),
143            Self::QuickAck(e) => write!(f, "quickack: {e}"),
144            Self::Cpu(e) => write!(f, "cpu: {e}"),
145            Self::HostCheck(e) => write!(f, "host check: {e}"),
146        }
147    }
148}
149
150impl std::error::Error for Error {
151    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
152        match self {
153            Self::Io(e) => Some(e),
154            Self::Tls(e) => Some(e),
155            Self::QuickAck(e) => Some(e.as_ref()),
156            Self::Cpu(e) => Some(e.as_ref()),
157            Self::HostCheck(e) => Some(e.as_ref()),
158            Self::Unanswered(u) => Some(&u.cause),
159            _ => None,
160        }
161    }
162}
163
164impl From<std::io::Error> for Error {
165    fn from(e: std::io::Error) -> Self {
166        Self::Io(e)
167    }
168}
169
170impl From<crate::placement::CpuError> for Error {
171    fn from(e: crate::placement::CpuError) -> Self {
172        Self::Cpu(Box::new(e))
173    }
174}
175
176impl From<ergon_tune::HostCheckError> for Error {
177    fn from(e: ergon_tune::HostCheckError) -> Self {
178        Self::HostCheck(Box::new(e))
179    }
180}
181
182impl From<rustls::Error> for Error {
183    fn from(e: rustls::Error) -> Self {
184        Self::Tls(e)
185    }
186}