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