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 http::StatusCode;
7use url::Url;
8
9use crate::core::retry::GATEWAY_STATUSES;
10use crate::h2::H2Error;
11use crate::h2::error::ErrorCode;
12#[cfg(feature = "http3")]
13use crate::quic::H3SendError;
14use crate::tls::TlsError;
15
16pub type Result<T> = std::result::Result<T, Error>;
17
18type Source = Box<dyn StdError + Send + Sync>;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum Kind {
23    Request,
24    Redirect,
25    Status,
26    Body,
27    Decode,
28    Timeout,
29    Connect,
30    Tls,
31    Http2,
32    Http3,
33    Proxy,
34    Io,
35    Config,
36    Url,
37    Json,
38}
39
40impl Kind {
41    pub fn as_str(self) -> &'static str {
42        match self {
43            Kind::Request => "request",
44            Kind::Redirect => "redirect",
45            Kind::Status => "status",
46            Kind::Body => "body",
47            Kind::Decode => "decode",
48            Kind::Timeout => "timeout",
49            Kind::Connect => "connect",
50            Kind::Tls => "tls",
51            Kind::Http2 => "http2",
52            Kind::Http3 => "http3",
53            Kind::Proxy => "proxy",
54            Kind::Io => "io",
55            Kind::Config => "config",
56            Kind::Url => "url",
57            Kind::Json => "json",
58        }
59    }
60}
61
62impl fmt::Display for Kind {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.write_str(self.as_str())
65    }
66}
67
68struct Inner {
69    kind: Kind,
70    source: Option<Source>,
71    url: Option<Url>,
72    status: Option<StatusCode>,
73    message: Option<Cow<'static, str>>,
74    alpn: Option<String>,
75}
76
77pub struct Error {
78    inner: Box<Inner>,
79}
80
81impl Error {
82    pub(crate) fn new(kind: Kind) -> Self {
83        Self {
84            inner: Box::new(Inner {
85                kind,
86                source: None,
87                url: None,
88                status: None,
89                message: None,
90                alpn: None,
91            }),
92        }
93    }
94
95    pub(crate) fn with_source(mut self, source: impl Into<Source>) -> Self {
96        self.inner.source = Some(source.into());
97        self
98    }
99
100    pub(crate) fn with_url(mut self, url: Url) -> Self {
101        self.inner.url = Some(url);
102        self
103    }
104
105    pub(crate) fn with_status(mut self, status: StatusCode) -> Self {
106        self.inner.status = Some(status);
107        self
108    }
109
110    pub(crate) fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
111        self.inner.message = Some(message.into());
112        self
113    }
114
115    pub(crate) fn with_alpn(mut self, negotiated: impl Into<String>) -> Self {
116        self.inner.alpn = Some(negotiated.into());
117        self
118    }
119
120    pub fn kind(&self) -> Kind {
121        self.inner.kind
122    }
123
124    pub(crate) fn message(&self) -> Option<&str> {
125        self.inner.message.as_deref()
126    }
127
128    pub fn status(&self) -> Option<StatusCode> {
129        self.inner.status
130    }
131
132    pub fn url(&self) -> Option<&Url> {
133        self.inner.url.as_ref()
134    }
135
136    pub fn is_timeout(&self) -> bool {
137        self.inner.kind == Kind::Timeout
138            || self
139                .io_cause()
140                .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut)
141    }
142
143    pub fn is_connect(&self) -> bool {
144        if self.inner.kind == Kind::Connect {
145            return true;
146        }
147        if matches!(
148            self.tls(),
149            Some(
150                TlsError::TcpConnect(_)
151                    | TlsError::Dns(_)
152                    | TlsError::Handshake(_)
153                    | TlsError::HandshakeIo(_)
154                    | TlsError::Rejected(_)
155            )
156        ) {
157            return true;
158        }
159        self.io().is_some_and(|e| {
160            matches!(
161                e.kind(),
162                io::ErrorKind::ConnectionRefused
163                    | io::ErrorKind::AddrNotAvailable
164                    | io::ErrorKind::NotConnected
165                    | io::ErrorKind::NetworkUnreachable
166            )
167        })
168    }
169
170    pub fn is_status(&self) -> bool {
171        self.inner.kind == Kind::Status
172    }
173
174    pub fn is_retryable(&self) -> bool {
175        self.is_timeout()
176            || self.is_connect()
177            || self.is_connection_closed()
178            || self.is_proxy_transient()
179    }
180
181    fn is_proxy_transient(&self) -> bool {
182        let Some(TlsError::Proxy { status, source, .. }) = self.tls() else {
183            return false;
184        };
185        source.is_some() || status.is_some_and(|code| GATEWAY_STATUSES.contains(&code))
186    }
187
188    pub(crate) fn is_connection_closed(&self) -> bool {
189        if self.io().is_some_and(|e| {
190            matches!(
191                e.kind(),
192                io::ErrorKind::UnexpectedEof
193                    | io::ErrorKind::ConnectionReset
194                    | io::ErrorKind::ConnectionAborted
195                    | io::ErrorKind::BrokenPipe
196            )
197        }) {
198            return true;
199        }
200        if matches!(
201            self.h2(),
202            Some(
203                H2Error::Io(_)
204                    | H2Error::Connection {
205                        code: ErrorCode::NoError,
206                        ..
207                    }
208                    | H2Error::Stream {
209                        code: ErrorCode::RefusedStream,
210                        ..
211                    }
212            )
213        ) {
214            return true;
215        }
216        matches!(
217            self.tls(),
218            Some(TlsError::Handshake(_) | TlsError::HandshakeIo(_) | TlsError::Rejected(_))
219        )
220    }
221
222    fn io_cause(&self) -> Option<&io::Error> {
223        self.io()
224            .or_else(|| self.tls().and_then(TlsError::io_source))
225    }
226
227    pub fn io(&self) -> Option<&io::Error> {
228        self.source_as()
229    }
230
231    pub fn tls(&self) -> Option<&TlsError> {
232        self.source_as()
233    }
234
235    pub fn h2(&self) -> Option<&H2Error> {
236        self.source_as()
237    }
238
239    pub(crate) fn alpn(&self) -> Option<&str> {
240        self.inner.alpn.as_deref()
241    }
242
243    fn source_as<T: StdError + 'static>(&self) -> Option<&T> {
244        self.inner.source.as_ref()?.downcast_ref::<T>()
245    }
246}
247
248impl fmt::Display for Error {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.write_str(self.inner.kind.as_str())?;
251        if let Some(status) = self.inner.status {
252            write!(f, " {}", status.as_u16())?;
253        }
254        if let Some(message) = &self.inner.message {
255            write!(f, ": {message}")?;
256        }
257        if let Some(url) = &self.inner.url {
258            write!(f, " for {}", crate::util::redact(url.as_str()))?;
259        }
260        Ok(())
261    }
262}
263
264impl fmt::Debug for Error {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        let mut out = f.debug_struct("leyline::Error");
267        out.field("kind", &self.inner.kind);
268        if let Some(status) = self.inner.status {
269            out.field("status", &status.as_u16());
270        }
271        if let Some(url) = &self.inner.url {
272            out.field("url", &crate::util::redact(url.as_str()));
273        }
274        if let Some(message) = &self.inner.message {
275            out.field("message", message);
276        }
277        if let Some(source) = &self.inner.source {
278            out.field("source", source);
279        }
280        out.finish()
281    }
282}
283
284impl StdError for Error {
285    fn source(&self) -> Option<&(dyn StdError + 'static)> {
286        self.inner.source.as_ref().map(|e| &**e as &dyn StdError)
287    }
288}
289
290impl Error {
291    pub(crate) fn from_url_parse(e: url::ParseError) -> Self {
292        Error::new(Kind::Url).with_source(e)
293    }
294
295    pub(crate) fn from_json(e: serde_json::Error) -> Self {
296        Error::new(Kind::Json).with_source(e)
297    }
298
299    pub(crate) fn from_request_body(e: io::Error) -> Self {
300        Error::new(Kind::Body)
301            .with_message("request body stream failed")
302            .with_source(e)
303    }
304}
305
306impl From<io::Error> for Error {
307    fn from(e: io::Error) -> Self {
308        Error::new(Kind::Io).with_source(e)
309    }
310}
311
312impl From<TlsError> for Error {
313    fn from(e: TlsError) -> Self {
314        let kind = match &e {
315            TlsError::Dns(_) | TlsError::TcpConnect(_) => Kind::Connect,
316            TlsError::Proxy { .. } => Kind::Proxy,
317            TlsError::SslConfig(_) | TlsError::Profile(_) | TlsError::TrustStore(_) => Kind::Config,
318            _ => Kind::Tls,
319        };
320        Error::new(kind).with_source(e)
321    }
322}
323
324impl From<H2Error> for Error {
325    fn from(e: H2Error) -> Self {
326        if let Some(limit) = e.body_limit() {
327            return limit.error();
328        }
329        match e {
330            H2Error::RequestBody(io) => Error::from_request_body(io),
331            other => Error::new(Kind::Http2).with_source(other),
332        }
333    }
334}
335
336#[cfg(feature = "http3")]
337impl From<H3SendError> for Error {
338    fn from(e: H3SendError) -> Self {
339        match e {
340            H3SendError::BodyLimit(limit) => limit.error(),
341            H3SendError::RequestBody(io) => Error::from_request_body(io),
342            other => Error::new(Kind::Http3).with_message(other.message().into_owned()),
343        }
344    }
345}
346
347impl From<http::Error> for Error {
348    fn from(e: http::Error) -> Self {
349        Error::new(Kind::Request).with_source(e)
350    }
351}
352
353impl From<http::header::InvalidHeaderName> for Error {
354    fn from(e: http::header::InvalidHeaderName) -> Self {
355        Error::new(Kind::Request).with_source(e)
356    }
357}
358
359impl From<http::header::InvalidHeaderValue> for Error {
360    fn from(e: http::header::InvalidHeaderValue) -> Self {
361        Error::new(Kind::Request).with_source(e)
362    }
363}
364
365impl From<http::uri::InvalidUri> for Error {
366    fn from(e: http::uri::InvalidUri) -> Self {
367        Error::new(Kind::Url).with_source(e)
368    }
369}
370
371#[cfg(test)]
372#[path = "error/tests.rs"]
373mod tests;