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