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;