1#![allow(clippy::module_name_repetitions)]
2
3use std::borrow::Cow;
4
5pub const SERIALIZED_MESSAGE_CAP: usize = 4 * 1024;
6pub const SERIALIZED_CTX_CAP: usize = 1024;
7pub const SERIALIZED_CHAIN_MAX_ENTRIES: usize = 16;
8pub const SERIALIZED_CHAIN_ENTRY_CAP: usize = 1024;
9
10#[derive(thiserror::Error, Debug)]
11#[error("{kind}{}", .ctx.as_deref().map(|c| format!(": {c}")).unwrap_or_default())]
12pub struct Error {
13 pub kind: ErrorKind,
14 pub ctx: Option<Cow<'static, str>>,
15 #[source]
16 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
17}
18
19#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
20pub enum ErrorKind {
21 #[error("i/o")]
22 Io,
23 #[error("protocol")]
24 Protocol,
25 #[error("upstream: {0}")]
26 Upstream(UpstreamReason),
27 #[error("middleware")]
28 Middleware,
29 #[error("compile")]
30 Compile,
31 #[error("timeout: {0}")]
32 Timeout(TimeoutKind),
33 #[error("canceled")]
34 Canceled,
35 #[error("resource: {0}")]
36 Resource(ResourceKind),
37 #[error("internal")]
38 Internal,
39}
40
41#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
42pub enum UpstreamReason {
43 #[error("unreachable")]
44 Unreachable,
45 #[error("reset mid-request")]
46 ResetMidRequest,
47 #[error("reset on idle pickup")]
48 ResetOnIdlePickup,
49 #[error("tls handshake failed")]
50 TlsHandshake,
51 #[error("dns resolution failed")]
52 DnsFailure,
53 #[error("refused by upstream")]
54 Refused,
55 #[error("gone")]
56 Gone,
57 #[error("malformed response")]
58 Malformed,
59}
60
61#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
62pub enum TimeoutKind {
63 #[error("connect")]
64 Connect,
65 #[error("read")]
66 Read,
67 #[error("total")]
68 Total,
69 #[error("idle")]
70 Idle,
71 #[error("handshake")]
72 Handshake,
73}
74
75#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
76pub enum ResourceKind {
77 #[error("connection pool exhausted")]
78 ConnectionPool,
79 #[error("wasm pool exhausted")]
80 WasmPool,
81 #[error("memory budget exceeded")]
82 Memory,
83 #[error("file descriptors exhausted")]
84 FdExhausted,
85}
86
87impl Error {
88 #[must_use]
89 pub const fn new(kind: ErrorKind) -> Self {
90 Self { kind, ctx: None, source: None }
91 }
92
93 #[must_use]
94 pub fn with_ctx(mut self, ctx: impl Into<Cow<'static, str>>) -> Self {
95 self.ctx = Some(ctx.into());
96 self
97 }
98
99 #[must_use]
100 pub fn with_source<E: Into<Box<dyn std::error::Error + Send + Sync>>>(mut self, e: E) -> Self {
101 self.source = Some(e.into());
102 self
103 }
104
105 #[must_use]
106 pub fn io(msg: impl Into<Cow<'static, str>>) -> Self {
107 Self::new(ErrorKind::Io).with_ctx(msg)
108 }
109
110 #[must_use]
111 pub fn protocol(msg: impl Into<Cow<'static, str>>) -> Self {
112 Self::new(ErrorKind::Protocol).with_ctx(msg)
113 }
114
115 #[must_use]
116 pub const fn upstream(reason: UpstreamReason) -> Self {
117 Self::new(ErrorKind::Upstream(reason))
118 }
119
120 #[must_use]
121 pub fn middleware(msg: impl Into<Cow<'static, str>>) -> Self {
122 Self::new(ErrorKind::Middleware).with_ctx(msg)
123 }
124
125 #[must_use]
126 pub fn compile(msg: impl Into<Cow<'static, str>>) -> Self {
127 Self::new(ErrorKind::Compile).with_ctx(msg)
128 }
129
130 #[must_use]
131 pub const fn timeout(kind: TimeoutKind) -> Self {
132 Self::new(ErrorKind::Timeout(kind))
133 }
134
135 #[must_use]
136 pub const fn canceled() -> Self {
137 Self::new(ErrorKind::Canceled)
138 }
139
140 #[must_use]
141 pub const fn resource(kind: ResourceKind) -> Self {
142 Self::new(ErrorKind::Resource(kind))
143 }
144
145 #[must_use]
146 pub fn internal(msg: impl Into<Cow<'static, str>>) -> Self {
147 Self::new(ErrorKind::Internal).with_ctx(msg)
148 }
149
150 #[must_use]
151 pub const fn kind(&self) -> &ErrorKind {
152 &self.kind
153 }
154
155 #[must_use]
156 pub fn ctx(&self) -> Option<&str> {
157 self.ctx.as_deref()
158 }
159
160 #[must_use]
161 pub const fn kind_label(&self) -> &'static str {
162 match &self.kind {
163 ErrorKind::Io => "io",
164 ErrorKind::Protocol => "protocol",
165 ErrorKind::Upstream(_) => "upstream",
166 ErrorKind::Middleware => "middleware",
167 ErrorKind::Compile => "compile",
168 ErrorKind::Timeout(_) => "timeout",
169 ErrorKind::Canceled => "canceled",
170 ErrorKind::Resource(_) => "resource",
171 ErrorKind::Internal => "internal",
172 }
173 }
174
175 #[must_use]
176 pub const fn reason_label(&self) -> Option<&'static str> {
177 match &self.kind {
178 ErrorKind::Upstream(r) => Some(match r {
179 UpstreamReason::Unreachable => "unreachable",
180 UpstreamReason::ResetMidRequest => "reset_mid_request",
181 UpstreamReason::ResetOnIdlePickup => "reset_idle_pickup",
182 UpstreamReason::TlsHandshake => "tls_handshake",
183 UpstreamReason::DnsFailure => "dns_failure",
184 UpstreamReason::Refused => "refused",
185 UpstreamReason::Gone => "gone",
186 UpstreamReason::Malformed => "malformed",
187 }),
188 ErrorKind::Timeout(t) => Some(match t {
189 TimeoutKind::Connect => "connect",
190 TimeoutKind::Read => "read",
191 TimeoutKind::Total => "total",
192 TimeoutKind::Idle => "idle",
193 TimeoutKind::Handshake => "handshake",
194 }),
195 ErrorKind::Resource(r) => Some(match r {
196 ResourceKind::ConnectionPool => "connection_pool",
197 ResourceKind::WasmPool => "wasm_pool",
198 ResourceKind::Memory => "memory",
199 ResourceKind::FdExhausted => "fd_exhausted",
200 }),
201 _ => None,
202 }
203 }
204
205 #[must_use]
206 pub const fn is_retryable(&self) -> bool {
207 match &self.kind {
208 ErrorKind::Upstream(r) => matches!(
209 r,
210 UpstreamReason::Unreachable
211 | UpstreamReason::ResetOnIdlePickup
212 | UpstreamReason::DnsFailure
213 | UpstreamReason::Refused
214 | UpstreamReason::Gone
215 ),
216 ErrorKind::Timeout(TimeoutKind::Connect)
217 | ErrorKind::Resource(ResourceKind::ConnectionPool) => true,
218 _ => false,
219 }
220 }
221
222 #[must_use]
223 pub const fn http_status(&self) -> u16 {
224 match &self.kind {
225 ErrorKind::Protocol => 400,
226 ErrorKind::Upstream(_) => 502,
227 ErrorKind::Timeout(_) => 504,
228 ErrorKind::Resource(_) => 503,
229 ErrorKind::Canceled => 499,
230 ErrorKind::Middleware | ErrorKind::Compile | ErrorKind::Internal | ErrorKind::Io => 500,
231 }
232 }
233
234 #[must_use]
235 pub fn source_chain(&self) -> Vec<String> {
236 let mut out = Vec::new();
237 let mut cur: &dyn std::error::Error = self;
238 while let Some(src) = cur.source() {
239 out.push(src.to_string());
240 cur = src;
241 }
242 out
243 }
244}
245
246fn from_source<E>(kind: ErrorKind, e: E) -> Error
247where
248 E: std::error::Error + Send + Sync + 'static,
249{
250 Error { kind, ctx: None, source: Some(Box::new(e)) }
251}
252
253impl From<std::io::Error> for Error {
254 fn from(e: std::io::Error) -> Self {
255 from_source(ErrorKind::Io, e)
256 }
257}
258
259impl From<serde_json::Error> for Error {
260 fn from(e: serde_json::Error) -> Self {
261 from_source(ErrorKind::Compile, e)
262 }
263}
264
265impl From<fancy_regex::Error> for Error {
266 fn from(e: fancy_regex::Error) -> Self {
267 from_source(ErrorKind::Compile, e)
268 }
269}
270
271impl From<ipnet::AddrParseError> for Error {
272 fn from(e: ipnet::AddrParseError) -> Self {
273 from_source(ErrorKind::Compile, e)
274 }
275}
276
277impl From<tokio::time::error::Elapsed> for Error {
281 fn from(e: tokio::time::error::Elapsed) -> Self {
282 from_source(ErrorKind::Timeout(TimeoutKind::Total), e)
283 }
284}
285
286#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
292pub struct SerializedError {
293 pub kind: String,
294 pub reason: Option<String>,
295 pub message: String,
296 pub ctx: Option<String>,
297 pub source_chain: Vec<String>,
298 pub http_status: u16,
299 pub retryable: bool,
300}
301
302impl From<&Error> for SerializedError {
303 fn from(e: &Error) -> Self {
304 Self {
305 kind: e.kind_label().to_owned(),
306 reason: e.reason_label().map(ToOwned::to_owned),
307 message: cap_bytes(e.to_string(), SERIALIZED_MESSAGE_CAP),
308 ctx: e.ctx.as_deref().map(|c| cap_bytes(c.to_owned(), SERIALIZED_CTX_CAP)),
309 source_chain: cap_chain(e.source_chain()),
310 http_status: e.http_status(),
311 retryable: e.is_retryable(),
312 }
313 }
314}
315
316const TRUNC_SUFFIX: &str = "… [truncated]";
317
318fn cap_bytes(s: String, cap: usize) -> String {
319 if s.len() <= cap {
320 return s;
321 }
322 let budget = cap.saturating_sub(TRUNC_SUFFIX.len());
323 let mut end = budget.min(s.len());
324 while end > 0 && !s.is_char_boundary(end) {
325 end -= 1;
326 }
327 let mut out = String::with_capacity(end + TRUNC_SUFFIX.len());
328 out.push_str(&s[..end]);
329 out.push_str(TRUNC_SUFFIX);
330 out
331}
332
333fn cap_chain(chain: Vec<String>) -> Vec<String> {
334 if chain.len() <= SERIALIZED_CHAIN_MAX_ENTRIES {
335 return chain.into_iter().map(|s| cap_bytes(s, SERIALIZED_CHAIN_ENTRY_CAP)).collect();
336 }
337 let keep = SERIALIZED_CHAIN_MAX_ENTRIES - 1;
338 let dropped = chain.len() - keep;
339 let mut out: Vec<String> =
340 chain.into_iter().take(keep).map(|s| cap_bytes(s, SERIALIZED_CHAIN_ENTRY_CAP)).collect();
341 out.push(format!("… [{dropped} more]"));
342 out
343}