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