reqwest/
error.rs

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