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