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;