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}