Skip to main content

leyline/core/
error.rs

1use std::borrow::Cow;
2use std::error::Error as StdError;
3use std::fmt;
4use std::io;
5
6use bytes::Bytes;
7use http::{HeaderMap, StatusCode};
8use url::Url;
9
10use crate::h2::H2Error;
11use crate::tls::TlsError;
12
13mod category;
14mod classify;
15mod from;
16mod marker;
17mod profile;
18
19pub(crate) use profile::ProfileChanged;
20
21pub use category::ErrorCategory;
22
23pub type Result<T> = std::result::Result<T, Error>;
24
25type Source = Box<dyn StdError + Send + Sync>;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum Kind {
30    Request,
31    Redirect,
32    Status,
33    Body,
34    Decode,
35    Timeout,
36    Connect,
37    Tls,
38    Http2,
39    Http3,
40    Proxy,
41    Io,
42    Config,
43    Url,
44    Json,
45}
46
47impl Kind {
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Kind::Request => "request",
51            Kind::Redirect => "redirect",
52            Kind::Status => "status",
53            Kind::Body => "body",
54            Kind::Decode => "decode",
55            Kind::Timeout => "timeout",
56            Kind::Connect => "connect",
57            Kind::Tls => "tls",
58            Kind::Http2 => "http2",
59            Kind::Http3 => "http3",
60            Kind::Proxy => "proxy",
61            Kind::Io => "io",
62            Kind::Config => "config",
63            Kind::Url => "url",
64            Kind::Json => "json",
65        }
66    }
67}
68
69impl fmt::Display for Kind {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.write_str(self.as_str())
72    }
73}
74
75struct Inner {
76    kind: Kind,
77    source: Option<Source>,
78    url: Option<Url>,
79    status: Option<StatusCode>,
80    message: Option<Cow<'static, str>>,
81    alpn: Option<String>,
82    body: Option<Bytes>,
83    headers: Option<HeaderMap>,
84    attempts: u32,
85    proxy: Option<String>,
86    policy_wait: Option<std::time::Duration>,
87    retries_exhausted: bool,
88}
89
90pub struct Error {
91    inner: Box<Inner>,
92}
93
94impl Error {
95    pub(crate) fn new(kind: Kind) -> Self {
96        Self {
97            inner: Box::new(Inner {
98                kind,
99                source: None,
100                url: None,
101                status: None,
102                message: None,
103                alpn: None,
104                body: None,
105                headers: None,
106                attempts: 0,
107                proxy: None,
108                policy_wait: None,
109                retries_exhausted: false,
110            }),
111        }
112    }
113
114    pub(crate) fn with_source(mut self, source: impl Into<Source>) -> Self {
115        self.inner.source = Some(source.into());
116        self
117    }
118
119    pub(crate) fn with_url(mut self, url: Url) -> Self {
120        self.inner.url = Some(url);
121        self
122    }
123
124    pub(crate) fn with_status(mut self, status: StatusCode) -> Self {
125        self.inner.status = Some(status);
126        self
127    }
128
129    pub(crate) fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
130        self.inner.message = Some(message.into());
131        self
132    }
133
134    pub(crate) fn with_alpn(mut self, negotiated: impl Into<String>) -> Self {
135        self.inner.alpn = Some(negotiated.into());
136        self
137    }
138
139    pub(crate) fn with_body(mut self, body: impl Into<Bytes>) -> Self {
140        self.inner.body = Some(body.into());
141        self
142    }
143
144    pub(crate) fn with_headers(mut self, headers: HeaderMap) -> Self {
145        self.inner.headers = Some(headers);
146        self
147    }
148
149    pub fn headers(&self) -> Option<&HeaderMap> {
150        self.inner.headers.as_ref()
151    }
152
153    pub fn header(&self, name: &str) -> Option<&str> {
154        self.headers()?.get(name).and_then(|v| v.to_str().ok())
155    }
156
157    pub fn attempts(&self) -> u32 {
158        self.inner.attempts
159    }
160
161    pub(crate) fn set_attempts(&mut self, attempts: u32) {
162        self.inner.attempts = attempts.max(1);
163    }
164
165    pub fn proxy(&self) -> Option<&str> {
166        self.inner.proxy.as_deref()
167    }
168
169    pub(crate) fn set_proxy(&mut self, proxy: Option<&str>) {
170        self.inner.proxy = proxy.map(crate::util::redact);
171    }
172
173    pub(crate) fn with_trail(mut self, attempts: u32, proxy: Option<String>) -> Self {
174        self.inner.attempts = attempts;
175        self.inner.proxy = proxy;
176        self
177    }
178
179    pub fn retries_exhausted(&self) -> bool {
180        self.inner.retries_exhausted
181    }
182
183    pub(crate) fn set_retry_outcome(
184        &mut self,
185        policy_wait: Option<std::time::Duration>,
186        retries_exhausted: bool,
187    ) {
188        self.inner.policy_wait = policy_wait;
189        self.inner.retries_exhausted = retries_exhausted;
190    }
191
192    pub fn body(&self) -> Option<&[u8]> {
193        self.inner.body.as_deref()
194    }
195
196    pub fn body_text(&self) -> Option<Cow<'_, str>> {
197        self.body().map(String::from_utf8_lossy)
198    }
199
200    pub fn retry_after(&self) -> Option<std::time::Duration> {
201        self.status()?;
202        if self.inner.policy_wait.is_some() {
203            return self.inner.policy_wait;
204        }
205        self.header(http::header::RETRY_AFTER.as_str())
206            .and_then(crate::core::retry::parse_retry_after)
207    }
208
209    pub fn find<'a>(error: &'a (dyn StdError + 'static)) -> Option<&'a Error> {
210        let mut current = Some(error);
211        while let Some(err) = current {
212            if let Some(found) = err.downcast_ref::<Error>() {
213                return Some(found);
214            }
215            current = err.source();
216        }
217        None
218    }
219
220    pub(crate) fn into_io(self) -> io::Error {
221        match self.body_limit() {
222            Some(limit) => limit.into_io(),
223            None => io::Error::other(self),
224        }
225    }
226
227    pub fn kind(&self) -> Kind {
228        self.inner.kind
229    }
230
231    pub(crate) fn message(&self) -> Option<&str> {
232        self.inner.message.as_deref()
233    }
234
235    pub fn status(&self) -> Option<StatusCode> {
236        self.inner.status
237    }
238
239    pub fn url(&self) -> Option<&Url> {
240        self.inner.url.as_ref()
241    }
242
243    pub fn io(&self) -> Option<&io::Error> {
244        self.source_as()
245    }
246
247    pub fn tls(&self) -> Option<&TlsError> {
248        self.source_as()
249    }
250
251    pub fn h2(&self) -> Option<&H2Error> {
252        self.source_as()
253    }
254
255    pub(crate) fn alpn(&self) -> Option<&str> {
256        self.inner.alpn.as_deref()
257    }
258
259    fn source_as<T: StdError + 'static>(&self) -> Option<&T> {
260        self.inner.source.as_ref()?.downcast_ref::<T>()
261    }
262}
263
264impl fmt::Display for Error {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        f.write_str(self.inner.kind.as_str())?;
267        if let Some(status) = self.inner.status {
268            write!(f, " {}", status.as_u16())?;
269        }
270        if let Some(message) = &self.inner.message {
271            write!(f, ": {message}")?;
272        }
273        if let Some(url) = &self.inner.url {
274            write!(f, " for {}", crate::util::redact(url.as_str()))?;
275        }
276        Ok(())
277    }
278}
279
280impl fmt::Debug for Error {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        let mut out = f.debug_struct("leyline::Error");
283        out.field("kind", &self.inner.kind);
284        if let Some(status) = self.inner.status {
285            out.field("status", &status.as_u16());
286        }
287        if let Some(url) = &self.inner.url {
288            out.field("url", &crate::util::redact(url.as_str()));
289        }
290        if let Some(message) = &self.inner.message {
291            out.field("message", message);
292        }
293        if let Some(source) = &self.inner.source {
294            out.field("source", source);
295        }
296        if let Some(body) = &self.inner.body {
297            out.field("body_len", &body.len());
298        }
299        if let Some(headers) = &self.inner.headers {
300            out.field("header_count", &headers.len());
301        }
302        if self.inner.attempts > 0 {
303            out.field("attempts", &self.inner.attempts);
304        }
305        if let Some(proxy) = &self.inner.proxy {
306            out.field("proxy", proxy);
307        }
308        if self.inner.retries_exhausted {
309            out.field("retries_exhausted", &true);
310        }
311        out.finish()
312    }
313}
314
315impl StdError for Error {
316    fn source(&self) -> Option<&(dyn StdError + 'static)> {
317        self.inner.source.as_ref().map(|e| &**e as &dyn StdError)
318    }
319}
320
321#[cfg(test)]
322#[path = "error/tests.rs"]
323mod tests;