Skip to main content

reqwest/
error.rs

1#![cfg_attr(
2    all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
3    allow(unused)
4)]
5use std::error::Error as StdError;
6use std::fmt;
7use std::io;
8
9use crate::util::Escape;
10use crate::{StatusCode, Url};
11
12/// A `Result` alias where the `Err` case is `reqwest::Error`.
13pub type Result<T> = std::result::Result<T, Error>;
14
15/// The Errors that may occur when processing a `Request`.
16///
17/// Note: Errors may include the full URL used to make the `Request`. If the URL
18/// contains sensitive information (e.g. an API key as a query parameter), be
19/// sure to remove it ([`without_url`](Error::without_url))
20pub struct Error {
21    inner: Box<Inner>,
22}
23
24pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
25
26struct Inner {
27    kind: Kind,
28    source: Option<BoxError>,
29    url: Option<Url>,
30}
31
32impl Error {
33    pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
34    where
35        E: Into<BoxError>,
36    {
37        Error {
38            inner: Box::new(Inner {
39                kind,
40                source: source.map(Into::into),
41                url: None,
42            }),
43        }
44    }
45
46    /// Returns a possible URL related to this error.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// # async fn run() {
52    /// // displays last stop of a redirect loop
53    /// let response = reqwest::get("http://site.with.redirect.loop").await;
54    /// if let Err(e) = response {
55    ///     if e.is_redirect() {
56    ///         if let Some(final_stop) = e.url() {
57    ///             println!("redirect loop at {final_stop}");
58    ///         }
59    ///     }
60    /// }
61    /// # }
62    /// ```
63    pub fn url(&self) -> Option<&Url> {
64        self.inner.url.as_ref()
65    }
66
67    /// Returns a mutable reference to the URL related to this error
68    ///
69    /// This is useful if you need to remove sensitive information from the URL
70    /// (e.g. an API key in the query), but do not want to remove the URL
71    /// entirely.
72    pub fn url_mut(&mut self) -> Option<&mut Url> {
73        self.inner.url.as_mut()
74    }
75
76    /// Add a url related to this error (overwriting any existing)
77    pub fn with_url(mut self, url: Url) -> Self {
78        self.inner.url = Some(url);
79        self
80    }
81
82    pub(crate) fn if_no_url(mut self, f: impl FnOnce() -> Url) -> Self {
83        if self.inner.url.is_none() {
84            self.inner.url = Some(f());
85        }
86        self
87    }
88
89    /// Strip the related url from this error (if, for example, it contains
90    /// sensitive information)
91    pub fn without_url(mut self) -> Self {
92        self.inner.url = None;
93        self
94    }
95
96    /// Returns true if the error is from a type Builder.
97    pub fn is_builder(&self) -> bool {
98        matches!(self.inner.kind, Kind::Builder)
99    }
100
101    /// Returns true if the error is from a `RedirectPolicy`.
102    pub fn is_redirect(&self) -> bool {
103        matches!(self.inner.kind, Kind::Redirect)
104    }
105
106    /// Returns true if the error is from `Response::error_for_status`.
107    pub fn is_status(&self) -> bool {
108        #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
109        {
110            matches!(self.inner.kind, Kind::Status(_, _))
111        }
112        #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
113        {
114            matches!(self.inner.kind, Kind::Status(_))
115        }
116    }
117
118    /// Returns true if the error is related to a timeout.
119    pub fn is_timeout(&self) -> bool {
120        let mut source = self.source();
121
122        while let Some(err) = source {
123            if err.is::<TimedOut>() {
124                return true;
125            }
126            if let Some(err) = err.downcast_ref::<Error>() {
127                if err.is_timeout() {
128                    return true;
129                }
130            }
131            #[cfg(not(all(
132                target_arch = "wasm32",
133                any(target_os = "unknown", target_os = "none")
134            )))]
135            if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
136                if hyper_err.is_timeout() {
137                    return true;
138                }
139            }
140            if let Some(io) = err.downcast_ref::<io::Error>() {
141                if io.kind() == io::ErrorKind::TimedOut {
142                    return true;
143                }
144            }
145            source = err.source();
146        }
147
148        false
149    }
150
151    /// Returns true if the error is related to the request
152    pub fn is_request(&self) -> bool {
153        matches!(self.inner.kind, Kind::Request)
154    }
155
156    #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
157    /// Returns true if the error is related to connect
158    pub fn is_connect(&self) -> bool {
159        let mut source = self.source();
160
161        while let Some(err) = source {
162            if let Some(hyper_err) = err.downcast_ref::<hyper_util::client::legacy::Error>() {
163                if hyper_err.is_connect() {
164                    return true;
165                }
166            }
167
168            source = err.source();
169        }
170
171        false
172    }
173
174    #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
175    /// Returns true if the error is related to DNS resolution.
176    pub fn is_dns(&self) -> bool {
177        let mut source = self.source();
178
179        while let Some(err) = source {
180            if err.is::<DnsError>() {
181                return true;
182            }
183
184            source = err.source();
185        }
186
187        false
188    }
189
190    /// Returns true if the error is related to the request or response body
191    pub fn is_body(&self) -> bool {
192        matches!(self.inner.kind, Kind::Body)
193    }
194
195    /// Returns true if the error is related to decoding the response's body
196    pub fn is_decode(&self) -> bool {
197        matches!(self.inner.kind, Kind::Decode)
198    }
199
200    /// Returns the status code, if the error was generated from a response.
201    pub fn status(&self) -> Option<StatusCode> {
202        match self.inner.kind {
203            #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
204            Kind::Status(code) => Some(code),
205            #[cfg(not(all(
206                target_arch = "wasm32",
207                any(target_os = "unknown", target_os = "none")
208            )))]
209            Kind::Status(code, _) => Some(code),
210            _ => None,
211        }
212    }
213
214    /// Returns true if the error is related to a protocol upgrade request
215    pub fn is_upgrade(&self) -> bool {
216        matches!(self.inner.kind, Kind::Upgrade)
217    }
218
219    // private
220
221    #[allow(unused)]
222    pub(crate) fn into_io(self) -> io::Error {
223        io::Error::other(self)
224    }
225}
226
227/// Converts from external types to reqwest's
228/// internal equivalents.
229///
230/// Currently only is used for `tower::timeout::error::Elapsed`.
231#[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
232pub(crate) fn cast_to_internal_error(error: BoxError) -> BoxError {
233    if error.is::<tower::timeout::error::Elapsed>() {
234        Box::new(crate::error::TimedOut) as BoxError
235    } else {
236        error
237    }
238}
239
240impl fmt::Debug for Error {
241    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
242        let mut builder = f.debug_struct("reqwest::Error");
243
244        builder.field("kind", &self.inner.kind);
245
246        if let Some(ref url) = self.inner.url {
247            builder.field("url", &url.as_str());
248        }
249        if let Some(ref source) = self.inner.source {
250            builder.field("source", source);
251        }
252
253        builder.finish()
254    }
255}
256
257impl fmt::Display for Error {
258    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
259        match self.inner.kind {
260            Kind::Builder => f.write_str("builder error")?,
261            Kind::Request => f.write_str("error sending request")?,
262            Kind::Body => f.write_str("request or response body error")?,
263            Kind::Decode => f.write_str("error decoding response body")?,
264            Kind::Redirect => f.write_str("error following redirect")?,
265            Kind::Upgrade => f.write_str("error upgrading connection")?,
266            #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
267            Kind::Status(ref code) => {
268                let prefix = if code.is_client_error() {
269                    "HTTP status client error"
270                } else {
271                    debug_assert!(code.is_server_error());
272                    "HTTP status server error"
273                };
274                write!(f, "{prefix} ({code})")?;
275            }
276            #[cfg(not(all(
277                target_arch = "wasm32",
278                any(target_os = "unknown", target_os = "none")
279            )))]
280            Kind::Status(ref code, ref reason) => {
281                let prefix = if code.is_client_error() {
282                    "HTTP status client error"
283                } else {
284                    debug_assert!(code.is_server_error());
285                    "HTTP status server error"
286                };
287                if let Some(reason) = reason {
288                    write!(
289                        f,
290                        "{prefix} ({} {})",
291                        code.as_str(),
292                        Escape::new(reason.as_bytes())
293                    )?;
294                } else {
295                    write!(f, "{prefix} ({code})")?;
296                }
297            }
298        };
299
300        if let Some(url) = &self.inner.url {
301            write!(f, " for url ({url})")?;
302        }
303
304        Ok(())
305    }
306}
307
308impl StdError for Error {
309    fn source(&self) -> Option<&(dyn StdError + 'static)> {
310        self.inner.source.as_ref().map(|e| &**e as _)
311    }
312}
313
314#[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
315impl From<crate::error::Error> for wasm_bindgen::JsValue {
316    fn from(err: Error) -> wasm_bindgen::JsValue {
317        js_sys::Error::from(err).into()
318    }
319}
320
321#[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
322impl From<crate::error::Error> for js_sys::Error {
323    fn from(err: Error) -> js_sys::Error {
324        js_sys::Error::new(&format!("{err}"))
325    }
326}
327
328#[derive(Debug)]
329pub(crate) enum Kind {
330    Builder,
331    Request,
332    Redirect,
333    #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
334    Status(StatusCode, Option<hyper::ext::ReasonPhrase>),
335    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
336    Status(StatusCode),
337    Body,
338    Decode,
339    Upgrade,
340}
341
342// constructors
343
344pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
345    Error::new(Kind::Builder, Some(e))
346}
347
348pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
349    Error::new(Kind::Body, Some(e))
350}
351
352pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
353    Error::new(Kind::Decode, Some(e))
354}
355
356pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
357    Error::new(Kind::Request, Some(e))
358}
359
360pub(crate) fn dns<E: Into<BoxError>>(e: E) -> BoxError {
361    Box::new(DnsError { inner: e.into() })
362}
363
364pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
365    Error::new(Kind::Redirect, Some(e)).with_url(url)
366}
367
368pub(crate) fn status_code(
369    url: Url,
370    status: StatusCode,
371    #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] reason: Option<hyper::ext::ReasonPhrase>,
372) -> Error {
373    Error::new(
374        Kind::Status(
375            status,
376            #[cfg(not(all(
377                target_arch = "wasm32",
378                any(target_os = "unknown", target_os = "none")
379            )))]
380            reason,
381        ),
382        None::<Error>,
383    )
384    .with_url(url)
385}
386
387pub(crate) fn url_bad_scheme(url: Url) -> Error {
388    Error::new(Kind::Builder, Some(BadScheme)).with_url(url)
389}
390
391pub(crate) fn url_invalid_uri(url: Url) -> Error {
392    Error::new(Kind::Builder, Some("Parsed Url is not a valid Uri")).with_url(url)
393}
394
395if_wasm! {
396    pub(crate) fn wasm(js_val: wasm_bindgen::JsValue) -> BoxError {
397        format!("{js_val:?}").into()
398    }
399}
400
401pub(crate) fn upgrade<E: Into<BoxError>>(e: E) -> Error {
402    Error::new(Kind::Upgrade, Some(e))
403}
404
405// io::Error helpers
406
407#[allow(unused)]
408pub(crate) fn decode_io(e: io::Error) -> Error {
409    if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
410        *e.into_inner()
411            .expect("io::Error::get_ref was Some(_)")
412            .downcast::<Error>()
413            .expect("StdError::is() was true")
414    } else {
415        decode(e)
416    }
417}
418
419// internal Error "sources"
420
421#[derive(Debug)]
422pub(crate) struct TimedOut;
423
424impl fmt::Display for TimedOut {
425    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
426        f.write_str("operation timed out")
427    }
428}
429
430impl StdError for TimedOut {}
431
432#[derive(Debug)]
433pub(crate) struct BadScheme;
434
435impl fmt::Display for BadScheme {
436    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
437        f.write_str("URL scheme is not allowed")
438    }
439}
440
441impl StdError for BadScheme {}
442
443#[derive(Debug)]
444pub(crate) struct DnsError {
445    pub(crate) inner: BoxError,
446}
447
448impl fmt::Display for DnsError {
449    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
450        f.write_str("error resolving DNS")
451    }
452}
453
454impl StdError for DnsError {
455    fn source(&self) -> Option<&(dyn StdError + 'static)> {
456        Some(&*self.inner as _)
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    fn assert_send<T: Send>() {}
465    fn assert_sync<T: Sync>() {}
466
467    #[test]
468    fn test_source_chain() {
469        let root = Error::new(Kind::Request, None::<Error>);
470        assert!(root.source().is_none());
471
472        let link = super::body(root);
473        assert!(link.source().is_some());
474        assert_send::<Error>();
475        assert_sync::<Error>();
476    }
477
478    #[test]
479    fn mem_size_of() {
480        use std::mem::size_of;
481        assert_eq!(size_of::<Error>(), size_of::<usize>());
482    }
483
484    #[test]
485    fn roundtrip_io_error() {
486        let orig = super::request("orig");
487        // Convert reqwest::Error into an io::Error...
488        let io = orig.into_io();
489        // Convert that io::Error back into a reqwest::Error...
490        let err = super::decode_io(io);
491        // It should have pulled out the original, not nested it...
492        match err.inner.kind {
493            Kind::Request => (),
494            _ => panic!("{err:?}"),
495        }
496    }
497
498    #[test]
499    fn from_unknown_io_error() {
500        let orig = io::Error::other("orly");
501        let err = super::decode_io(orig);
502        match err.inner.kind {
503            Kind::Decode => (),
504            _ => panic!("{err:?}"),
505        }
506    }
507
508    #[test]
509    fn decode_body_timeout_is_timeout() {
510        // A body timeout surfaced while decoding stays a decode error, but
511        // is_timeout still finds the timeout in the source chain.
512        let err = super::decode(super::body(super::TimedOut));
513        assert!(err.is_decode());
514        assert!(err.is_timeout());
515    }
516
517    #[test]
518    fn decode_wraps_other_errors() {
519        let io = io::Error::other("boom");
520        let err = super::decode(io);
521        assert!(err.is_decode());
522        assert!(!err.is_timeout());
523    }
524
525    #[test]
526    fn is_timeout() {
527        let err = super::request(super::TimedOut);
528        assert!(err.is_timeout());
529
530        // todo: test `hyper::Error::is_timeout` when we can easily construct one
531
532        let io = io::Error::from(io::ErrorKind::TimedOut);
533        let nested = super::request(io);
534        assert!(nested.is_timeout());
535    }
536
537    #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
538    #[test]
539    fn is_dns() {
540        let err = super::request(DnsError { inner: "".into() });
541        assert!(err.is_dns());
542    }
543}