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    after_response: bool,
89}
90
91pub struct Error {
92    inner: Box<Inner>,
93}
94
95impl Error {
96    pub(crate) fn new(kind: Kind) -> Self {
97        Self {
98            inner: Box::new(Inner {
99                kind,
100                source: None,
101                url: None,
102                status: None,
103                message: None,
104                alpn: None,
105                body: None,
106                headers: None,
107                attempts: 0,
108                proxy: None,
109                policy_wait: None,
110                retries_exhausted: false,
111                after_response: false,
112            }),
113        }
114    }
115
116    pub(crate) fn with_source(mut self, source: impl Into<Source>) -> Self {
117        self.inner.source = Some(source.into());
118        self
119    }
120
121    pub(crate) fn with_url(mut self, url: Url) -> Self {
122        self.inner.url = Some(url);
123        self
124    }
125
126    pub(crate) fn with_status(mut self, status: StatusCode) -> Self {
127        self.inner.status = Some(status);
128        self
129    }
130
131    pub(crate) fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
132        self.inner.message = Some(message.into());
133        self
134    }
135
136    pub(crate) fn with_alpn(mut self, negotiated: impl Into<String>) -> Self {
137        self.inner.alpn = Some(negotiated.into());
138        self
139    }
140
141    pub(crate) fn with_body(mut self, body: impl Into<Bytes>) -> Self {
142        self.inner.body = Some(body.into());
143        self
144    }
145
146    pub(crate) fn with_headers(mut self, headers: HeaderMap) -> Self {
147        self.inner.headers = Some(headers);
148        self
149    }
150
151    pub(crate) fn without_content_coding(mut self) -> Self {
152        if let Some(headers) = self.inner.headers.as_mut() {
153            headers.remove(http::header::CONTENT_ENCODING);
154        }
155        self.without_content_length()
156    }
157
158    pub(crate) fn without_content_length(mut self) -> Self {
159        if let Some(headers) = self.inner.headers.as_mut() {
160            headers.remove(http::header::CONTENT_LENGTH);
161        }
162        self
163    }
164
165    pub(crate) fn after_response(mut self) -> Self {
166        self.inner.after_response = true;
167        self
168    }
169
170    pub(crate) fn follows_response(&self) -> bool {
171        self.inner.after_response
172    }
173
174    pub fn headers(&self) -> Option<&HeaderMap> {
175        self.inner.headers.as_ref()
176    }
177
178    pub fn header(&self, name: &str) -> Option<&str> {
179        self.headers()?.get(name).and_then(|v| v.to_str().ok())
180    }
181
182    pub fn attempts(&self) -> u32 {
183        self.inner.attempts
184    }
185
186    pub(crate) fn set_attempts(&mut self, attempts: u32) {
187        self.inner.attempts = attempts.max(1);
188    }
189
190    pub fn proxy(&self) -> Option<&str> {
191        self.inner.proxy.as_deref()
192    }
193
194    pub(crate) fn set_proxy(&mut self, proxy: Option<&str>) {
195        self.inner.proxy = proxy.map(crate::util::redact);
196    }
197
198    pub(crate) fn with_trail(mut self, attempts: u32, proxy: Option<String>) -> Self {
199        self.inner.attempts = attempts;
200        self.inner.proxy = proxy;
201        self
202    }
203
204    pub fn retries_exhausted(&self) -> bool {
205        self.inner.retries_exhausted
206    }
207
208    pub(crate) fn set_retry_outcome(
209        &mut self,
210        policy_wait: Option<std::time::Duration>,
211        retries_exhausted: bool,
212    ) {
213        self.inner.policy_wait = policy_wait;
214        self.inner.retries_exhausted = retries_exhausted;
215    }
216
217    pub fn body(&self) -> Option<&[u8]> {
218        self.inner.body.as_deref()
219    }
220
221    pub fn body_text(&self) -> Option<Cow<'_, str>> {
222        self.body().map(String::from_utf8_lossy)
223    }
224
225    pub fn retry_after(&self) -> Option<std::time::Duration> {
226        self.status()?;
227        if self.inner.policy_wait.is_some() {
228            return self.inner.policy_wait;
229        }
230        self.header(http::header::RETRY_AFTER.as_str())
231            .and_then(crate::core::retry::parse_retry_after)
232    }
233
234    pub fn find<'a>(error: &'a (dyn StdError + 'static)) -> Option<&'a Error> {
235        let mut current = Some(error);
236        while let Some(err) = current {
237            if let Some(found) = err.downcast_ref::<Error>() {
238                return Some(found);
239            }
240            current = err.source();
241        }
242        None
243    }
244
245    pub(crate) fn into_io(self) -> io::Error {
246        match self.body_limit() {
247            Some(limit) => limit.into_io(),
248            None => io::Error::other(self),
249        }
250    }
251
252    pub fn kind(&self) -> Kind {
253        self.inner.kind
254    }
255
256    pub(crate) fn message(&self) -> Option<&str> {
257        self.inner.message.as_deref()
258    }
259
260    pub fn status(&self) -> Option<StatusCode> {
261        self.inner.status
262    }
263
264    pub fn url(&self) -> Option<&Url> {
265        self.inner.url.as_ref()
266    }
267
268    pub fn io(&self) -> Option<&io::Error> {
269        self.source_as()
270    }
271
272    pub fn tls(&self) -> Option<&TlsError> {
273        self.source_as()
274    }
275
276    pub fn h2(&self) -> Option<&H2Error> {
277        self.source_as()
278    }
279
280    pub(crate) fn alpn(&self) -> Option<&str> {
281        self.inner.alpn.as_deref()
282    }
283
284    fn source_as<T: StdError + 'static>(&self) -> Option<&T> {
285        self.inner.source.as_ref()?.downcast_ref::<T>()
286    }
287}
288
289impl fmt::Display for Error {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.write_str(self.inner.kind.as_str())?;
292        if let Some(status) = self.inner.status {
293            write!(f, " {}", status.as_u16())?;
294        }
295        if let Some(message) = &self.inner.message {
296            write!(f, ": {message}")?;
297        }
298        if let Some(url) = &self.inner.url {
299            write!(f, " for {}", crate::util::redact(url.as_str()))?;
300        }
301        Ok(())
302    }
303}
304
305impl fmt::Debug for Error {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        let mut out = f.debug_struct("leyline::Error");
308        out.field("kind", &self.inner.kind);
309        if let Some(status) = self.inner.status {
310            out.field("status", &status.as_u16());
311        }
312        if let Some(url) = &self.inner.url {
313            out.field("url", &crate::util::redact(url.as_str()));
314        }
315        if let Some(message) = &self.inner.message {
316            out.field("message", message);
317        }
318        if let Some(source) = &self.inner.source {
319            out.field("source", source);
320        }
321        if let Some(body) = &self.inner.body {
322            out.field("body_len", &body.len());
323        }
324        if let Some(headers) = &self.inner.headers {
325            out.field("header_count", &headers.len());
326        }
327        if self.inner.attempts > 0 {
328            out.field("attempts", &self.inner.attempts);
329        }
330        if let Some(proxy) = &self.inner.proxy {
331            out.field("proxy", proxy);
332        }
333        if self.inner.retries_exhausted {
334            out.field("retries_exhausted", &true);
335        }
336        out.finish()
337    }
338}
339
340impl StdError for Error {
341    fn source(&self) -> Option<&(dyn StdError + 'static)> {
342        self.inner.source.as_ref().map(|e| &**e as &dyn StdError)
343    }
344}
345
346#[cfg(test)]
347#[path = "error/tests.rs"]
348mod tests;