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;