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    pub(crate) fn if_no_url(mut self, f: impl FnOnce() -> Url) -> Self {
80        if self.inner.url.is_none() {
81            self.inner.url = Some(f());
82        }
83        self
84    }
85
86    /// Strip the related url from this error (if, for example, it contains
87    /// sensitive information)
88    pub fn without_url(mut self) -> Self {
89        self.inner.url = None;
90        self
91    }
92
93    /// Returns true if the error is from a type Builder.
94    pub fn is_builder(&self) -> bool {
95        matches!(self.inner.kind, Kind::Builder)
96    }
97
98    /// Returns true if the error is from a `RedirectPolicy`.
99    pub fn is_redirect(&self) -> bool {
100        matches!(self.inner.kind, Kind::Redirect)
101    }
102
103    /// Returns true if the error is from `Response::error_for_status`.
104    pub fn is_status(&self) -> bool {
105        #[cfg(not(target_arch = "wasm32"))]
106        {
107            matches!(self.inner.kind, Kind::Status(_, _))
108        }
109        #[cfg(target_arch = "wasm32")]
110        {
111            matches!(self.inner.kind, Kind::Status(_))
112        }
113    }
114
115    /// Returns true if the error is related to a timeout.
116    pub fn is_timeout(&self) -> bool {
117        let mut source = self.source();
118
119        while let Some(err) = source {
120            if err.is::<TimedOut>() {
121                return true;
122            }
123            #[cfg(not(target_arch = "wasm32"))]
124            if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
125                if hyper_err.is_timeout() {
126                    return true;
127                }
128            }
129            if let Some(io) = err.downcast_ref::<io::Error>() {
130                if io.kind() == io::ErrorKind::TimedOut {
131                    return true;
132                }
133            }
134            source = err.source();
135        }
136
137        false
138    }
139
140    /// Returns true if the error is related to the request
141    pub fn is_request(&self) -> bool {
142        matches!(self.inner.kind, Kind::Request)
143    }
144
145    #[cfg(not(target_arch = "wasm32"))]
146    /// Returns true if the error is related to connect
147    pub fn is_connect(&self) -> bool {
148        let mut source = self.source();
149
150        while let Some(err) = source {
151            if let Some(hyper_err) = err.downcast_ref::<hyper_util::client::legacy::Error>() {
152                if hyper_err.is_connect() {
153                    return true;
154                }
155            }
156
157            source = err.source();
158        }
159
160        false
161    }
162
163    /// Returns true if the error is related to the request or response body
164    pub fn is_body(&self) -> bool {
165        matches!(self.inner.kind, Kind::Body)
166    }
167
168    /// Returns true if the error is related to decoding the response's body
169    pub fn is_decode(&self) -> bool {
170        matches!(self.inner.kind, Kind::Decode)
171    }
172
173    /// Returns the status code, if the error was generated from a response.
174    pub fn status(&self) -> Option<StatusCode> {
175        match self.inner.kind {
176            #[cfg(target_arch = "wasm32")]
177            Kind::Status(code) => Some(code),
178            #[cfg(not(target_arch = "wasm32"))]
179            Kind::Status(code, _) => Some(code),
180            _ => None,
181        }
182    }
183
184    // private
185
186    #[allow(unused)]
187    pub(crate) fn into_io(self) -> io::Error {
188        io::Error::new(io::ErrorKind::Other, self)
189    }
190}
191
192/// Converts from external types to reqwest's
193/// internal equivalents.
194///
195/// Currently only is used for `tower::timeout::error::Elapsed`.
196#[cfg(not(target_arch = "wasm32"))]
197pub(crate) fn cast_to_internal_error(error: BoxError) -> BoxError {
198    if error.is::<tower::timeout::error::Elapsed>() {
199        Box::new(crate::error::TimedOut) as BoxError
200    } else {
201        error
202    }
203}
204
205impl fmt::Debug for Error {
206    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207        let mut builder = f.debug_struct("reqwest::Error");
208
209        builder.field("kind", &self.inner.kind);
210
211        if let Some(ref url) = self.inner.url {
212            builder.field("url", &url.as_str());
213        }
214        if let Some(ref source) = self.inner.source {
215            builder.field("source", source);
216        }
217
218        builder.finish()
219    }
220}
221
222impl fmt::Display for Error {
223    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
224        match self.inner.kind {
225            Kind::Builder => f.write_str("builder error")?,
226            Kind::Request => f.write_str("error sending request")?,
227            Kind::Body => f.write_str("request or response body error")?,
228            Kind::Decode => f.write_str("error decoding response body")?,
229            Kind::Redirect => f.write_str("error following redirect")?,
230            Kind::Upgrade => f.write_str("error upgrading connection")?,
231            #[cfg(target_arch = "wasm32")]
232            Kind::Status(ref code) => {
233                let prefix = if code.is_client_error() {
234                    "HTTP status client error"
235                } else {
236                    debug_assert!(code.is_server_error());
237                    "HTTP status server error"
238                };
239                write!(f, "{prefix} ({code})")?;
240            }
241            #[cfg(not(target_arch = "wasm32"))]
242            Kind::Status(ref code, ref reason) => {
243                let prefix = if code.is_client_error() {
244                    "HTTP status client error"
245                } else {
246                    debug_assert!(code.is_server_error());
247                    "HTTP status server error"
248                };
249                if let Some(reason) = reason {
250                    write!(
251                        f,
252                        "{prefix} ({} {})",
253                        code.as_str(),
254                        Escape::new(reason.as_bytes())
255                    )?;
256                } else {
257                    write!(f, "{prefix} ({code})")?;
258                }
259            }
260        };
261
262        if let Some(url) = &self.inner.url {
263            write!(f, " for url ({url})")?;
264        }
265
266        Ok(())
267    }
268}
269
270impl StdError for Error {
271    fn source(&self) -> Option<&(dyn StdError + 'static)> {
272        self.inner.source.as_ref().map(|e| &**e as _)
273    }
274}
275
276#[cfg(target_arch = "wasm32")]
277impl From<crate::error::Error> for wasm_bindgen::JsValue {
278    fn from(err: Error) -> wasm_bindgen::JsValue {
279        js_sys::Error::from(err).into()
280    }
281}
282
283#[cfg(target_arch = "wasm32")]
284impl From<crate::error::Error> for js_sys::Error {
285    fn from(err: Error) -> js_sys::Error {
286        js_sys::Error::new(&format!("{err}"))
287    }
288}
289
290#[derive(Debug)]
291pub(crate) enum Kind {
292    Builder,
293    Request,
294    Redirect,
295    #[cfg(not(target_arch = "wasm32"))]
296    Status(StatusCode, Option<hyper::ext::ReasonPhrase>),
297    #[cfg(target_arch = "wasm32")]
298    Status(StatusCode),
299    Body,
300    Decode,
301    Upgrade,
302}
303
304// constructors
305
306pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
307    Error::new(Kind::Builder, Some(e))
308}
309
310pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
311    Error::new(Kind::Body, Some(e))
312}
313
314pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
315    Error::new(Kind::Decode, Some(e))
316}
317
318pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
319    Error::new(Kind::Request, Some(e))
320}
321
322pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
323    Error::new(Kind::Redirect, Some(e)).with_url(url)
324}
325
326pub(crate) fn status_code(
327    url: Url,
328    status: StatusCode,
329    #[cfg(not(target_arch = "wasm32"))] reason: Option<hyper::ext::ReasonPhrase>,
330) -> Error {
331    Error::new(
332        Kind::Status(
333            status,
334            #[cfg(not(target_arch = "wasm32"))]
335            reason,
336        ),
337        None::<Error>,
338    )
339    .with_url(url)
340}
341
342pub(crate) fn url_bad_scheme(url: Url) -> Error {
343    Error::new(Kind::Builder, Some(BadScheme)).with_url(url)
344}
345
346pub(crate) fn url_invalid_uri(url: Url) -> Error {
347    Error::new(Kind::Builder, Some("Parsed Url is not a valid Uri")).with_url(url)
348}
349
350if_wasm! {
351    pub(crate) fn wasm(js_val: wasm_bindgen::JsValue) -> BoxError {
352        format!("{js_val:?}").into()
353    }
354}
355
356pub(crate) fn upgrade<E: Into<BoxError>>(e: E) -> Error {
357    Error::new(Kind::Upgrade, Some(e))
358}
359
360// io::Error helpers
361
362#[cfg(any(
363    feature = "gzip",
364    feature = "zstd",
365    feature = "brotli",
366    feature = "deflate",
367    feature = "blocking",
368))]
369pub(crate) fn into_io(e: BoxError) -> io::Error {
370    io::Error::new(io::ErrorKind::Other, e)
371}
372
373#[allow(unused)]
374pub(crate) fn decode_io(e: io::Error) -> Error {
375    if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
376        *e.into_inner()
377            .expect("io::Error::get_ref was Some(_)")
378            .downcast::<Error>()
379            .expect("StdError::is() was true")
380    } else {
381        decode(e)
382    }
383}
384
385// internal Error "sources"
386
387#[derive(Debug)]
388pub(crate) struct TimedOut;
389
390impl fmt::Display for TimedOut {
391    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
392        f.write_str("operation timed out")
393    }
394}
395
396impl StdError for TimedOut {}
397
398#[derive(Debug)]
399pub(crate) struct BadScheme;
400
401impl fmt::Display for BadScheme {
402    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
403        f.write_str("URL scheme is not allowed")
404    }
405}
406
407impl StdError for BadScheme {}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    fn assert_send<T: Send>() {}
414    fn assert_sync<T: Sync>() {}
415
416    #[test]
417    fn test_source_chain() {
418        let root = Error::new(Kind::Request, None::<Error>);
419        assert!(root.source().is_none());
420
421        let link = super::body(root);
422        assert!(link.source().is_some());
423        assert_send::<Error>();
424        assert_sync::<Error>();
425    }
426
427    #[test]
428    fn mem_size_of() {
429        use std::mem::size_of;
430        assert_eq!(size_of::<Error>(), size_of::<usize>());
431    }
432
433    #[test]
434    fn roundtrip_io_error() {
435        let orig = super::request("orig");
436        // Convert reqwest::Error into an io::Error...
437        let io = orig.into_io();
438        // Convert that io::Error back into a reqwest::Error...
439        let err = super::decode_io(io);
440        // It should have pulled out the original, not nested it...
441        match err.inner.kind {
442            Kind::Request => (),
443            _ => panic!("{err:?}"),
444        }
445    }
446
447    #[test]
448    fn from_unknown_io_error() {
449        let orig = io::Error::new(io::ErrorKind::Other, "orly");
450        let err = super::decode_io(orig);
451        match err.inner.kind {
452            Kind::Decode => (),
453            _ => panic!("{err:?}"),
454        }
455    }
456
457    #[test]
458    fn is_timeout() {
459        let err = super::request(super::TimedOut);
460        assert!(err.is_timeout());
461
462        // todo: test `hyper::Error::is_timeout` when we can easily construct one
463
464        let io = io::Error::from(io::ErrorKind::TimedOut);
465        let nested = super::request(io);
466        assert!(nested.is_timeout());
467    }
468}