ureq/
error.rs

1use url::{ParseError, Url};
2
3use std::error;
4use std::fmt::{self, Display};
5use std::io;
6
7use crate::Response;
8
9/// An error that may occur when processing a [Request](crate::Request).
10///
11/// This can represent connection-level errors (e.g. connection refused),
12/// protocol-level errors (malformed response), or status code errors
13/// (e.g. 404 Not Found). Status code errors are represented by the
14/// [Status](Error::Status) enum variant, while connection-level and
15/// protocol-level errors are represented by the [Transport](Error::Transport)
16/// enum variant. You can use a match statement to extract a Response
17/// from a `Status` error. For instance, you may want to read the full
18/// body of a response because you expect it to contain a useful error
19/// message. Or you may want to handle certain error code responses
20/// differently.
21///
22/// # Examples
23///
24/// Example of matching out all unexpected server status codes.
25///
26/// ```no_run
27/// use ureq::Error;
28///
29/// match ureq::get("http://mypage.example.com/").call() {
30///     Ok(response) => { /* it worked */},
31///     Err(Error::Status(code, response)) => {
32///         /* the server returned an unexpected status
33///            code (such as 400, 500 etc) */
34///     }
35///     Err(_) => { /* some kind of io/transport error */ }
36/// }
37/// ```
38///
39/// An example of a function that handles HTTP 429 and 500 errors differently
40/// than other errors. They get retried after a suitable delay, up to 4 times.
41///
42/// ```
43/// use std::{result::Result, time::Duration, thread};
44/// use ureq::{Response, Error, Error::Status};
45/// # fn main(){ ureq::is_test(true); get_response( "http://httpbin.org/status/500" ); }
46///
47/// fn get_response(url: &str) -> Result<Response, Error> {
48///     for _ in 1..4 {
49///         match ureq::get(url).call() {
50///             Err(Status(503, r)) | Err(Status(429, r)) => {
51///                 let retry: Option<u64> = r.header("retry-after")
52///                     .and_then(|h| h.parse().ok());
53///                 let retry = retry.unwrap_or(5);
54///                 eprintln!("{} for {}, retry in {}", r.status(), r.get_url(), retry);
55///                 thread::sleep(Duration::from_secs(retry));
56///             }
57///             result => return result,
58///         };
59///     }
60///     // Ran out of retries; try one last time and return whatever result we get.
61///     ureq::get(url).call()
62/// }
63/// ```
64///
65/// If you'd like to treat all status code errors as normal, successful responses,
66/// you can use [OrAnyStatus::or_any_status] like this:
67///
68/// ```
69/// use ureq::Error::Status;
70/// # fn main() -> std::result::Result<(), ureq::Transport> {
71/// # ureq::is_test(true);
72/// use ureq::OrAnyStatus;
73///
74/// let resp = ureq::get("http://example.com/")
75///   .call()
76///   .or_any_status()?;
77/// # Ok(())
78/// # }
79/// ```
80#[derive(Debug)]
81pub enum Error {
82    /// A response was successfully received but had status code >= 400.
83    /// Values are (status_code, Response).
84    Status(u16, Response),
85    /// There was an error making the request or receiving the response.
86    Transport(Transport),
87}
88
89impl Error {
90    /// Optionally turn this error into an underlying `Transport`.
91    ///
92    /// `None` if the underlying error is `Error::Status`.
93    pub fn into_transport(self) -> Option<Transport> {
94        match self {
95            Error::Status(_, _) => None,
96            Error::Transport(t) => Some(t),
97        }
98    }
99
100    /// Optionally turn this error into an underlying `Response`.
101    ///
102    /// `None` if the underlying error is `Error::Transport`.
103    pub fn into_response(self) -> Option<Response> {
104        match self {
105            Error::Status(_, r) => Some(r),
106            Error::Transport(_) => None,
107        }
108    }
109}
110
111/// Error that is not a status code error. For instance, DNS name not found,
112/// connection refused, or malformed response.
113///
114/// * [`Transport::kind()`] provides a classification (same as for [`Error::kind`]).
115/// * [`Transport::message()`] might vary for the same classification to give more context.
116/// * [`Transport::source()`](std::error::Error::source) holds the underlying error with even more details.
117///
118/// ```
119/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
120/// use ureq::ErrorKind;
121/// use std::error::Error;
122/// use url::ParseError;
123///
124/// let result = ureq::get("broken/url").call();
125/// let error = result.unwrap_err().into_transport().unwrap();
126///
127/// // the display trait is a combo of the underlying classifications
128/// assert_eq!(error.to_string(),
129///     "Bad URL: failed to parse URL: RelativeUrlWithoutBase: relative URL without a base");
130///
131/// // classification
132/// assert_eq!(error.kind(), ErrorKind::InvalidUrl);
133/// assert_eq!(error.kind().to_string(), "Bad URL");
134///
135/// // higher level message
136/// assert_eq!(error.message(), Some("failed to parse URL: RelativeUrlWithoutBase"));
137///
138/// // boxed underlying error
139/// let source = error.source().unwrap();
140/// // downcast to original error
141/// let downcast: &ParseError = source.downcast_ref().unwrap();
142///
143/// assert_eq!(downcast.to_string(), "relative URL without a base");
144/// # Ok(())
145/// # }
146/// ```
147#[derive(Debug)]
148pub struct Transport {
149    kind: ErrorKind,
150    message: Option<String>,
151    url: Option<Url>,
152    source: Option<Box<dyn error::Error + Send + Sync + 'static>>,
153}
154
155impl Transport {
156    /// The type of error that happened while processing the request.
157    pub fn kind(&self) -> ErrorKind {
158        self.kind
159    }
160
161    /// Higher level error details, if there are any.
162    pub fn message(&self) -> Option<&str> {
163        self.message.as_deref()
164    }
165
166    /// The url that failed. This can be interesting in cases of redirect where
167    /// the original url worked, but a later redirected to url fails.
168    pub fn url(&self) -> Option<&Url> {
169        self.url.as_ref()
170    }
171}
172
173/// Extension to [`Result<Response, Error>`] for handling all status codes as [`Response`].
174pub trait OrAnyStatus {
175    /// Ergonomic helper for handling all status codes as [`Response`].
176    ///
177    /// By default, ureq returns non-2xx responses as [`Error::Status`]. This
178    /// helper is for handling all responses as [`Response`], regardless
179    /// of status code.
180    ///
181    /// ```
182    /// # ureq::is_test(true);
183    /// # fn main() -> Result<(), ureq::Transport> {
184    /// // Bring trait into context.
185    /// use ureq::OrAnyStatus;
186    ///
187    /// let response = ureq::get("http://httpbin.org/status/500")
188    ///     .call()
189    ///     // Transport errors, such as DNS or connectivity problems
190    ///     // must still be dealt with as `Err`.
191    ///     .or_any_status()?;
192    ///
193    /// assert_eq!(response.status(), 500);
194    /// # Ok(())
195    /// # }
196    /// ```
197    fn or_any_status(self) -> Result<Response, Transport>;
198}
199
200impl OrAnyStatus for Result<Response, Error> {
201    fn or_any_status(self) -> Result<Response, Transport> {
202        match self {
203            Ok(response) => Ok(response),
204            Err(Error::Status(_, response)) => Ok(response),
205            Err(Error::Transport(transport)) => Err(transport),
206        }
207    }
208}
209
210impl Display for Error {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        match self {
213            Error::Status(status, response) => {
214                write!(f, "{}: status code {}", response.get_url(), status)?;
215                if let Some(original) = response.history.first() {
216                    write!(f, " (redirected from {})", original)?;
217                }
218            }
219            Error::Transport(err) => {
220                write!(f, "{}", err)?;
221            }
222        }
223        Ok(())
224    }
225}
226
227impl Display for Transport {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        if let Some(url) = &self.url {
230            write!(f, "{}: ", url)?;
231        }
232        write!(f, "{}", self.kind)?;
233        if let Some(message) = &self.message {
234            write!(f, ": {}", message)?;
235        }
236        if let Some(source) = &self.source {
237            write!(f, ": {}", source)?;
238        }
239        Ok(())
240    }
241}
242
243impl error::Error for Error {
244    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
245        match &self {
246            Error::Transport(Transport {
247                source: Some(s), ..
248            }) => Some(s.as_ref()),
249            _ => None,
250        }
251    }
252}
253
254impl error::Error for Transport {
255    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
256        self.source
257            .as_ref()
258            .map(|s| s.as_ref() as &(dyn error::Error + 'static))
259    }
260}
261
262impl Error {
263    pub(crate) fn new(kind: ErrorKind, message: Option<String>) -> Self {
264        Error::Transport(Transport {
265            kind,
266            message,
267            url: None,
268            source: None,
269        })
270    }
271
272    pub(crate) fn url(self, url: Url) -> Self {
273        if let Error::Transport(mut e) = self {
274            e.url = Some(url);
275            Error::Transport(e)
276        } else {
277            self
278        }
279    }
280
281    pub(crate) fn src(self, e: impl error::Error + Send + Sync + 'static) -> Self {
282        if let Error::Transport(mut oe) = self {
283            oe.source = Some(Box::new(e));
284            Error::Transport(oe)
285        } else {
286            self
287        }
288    }
289
290    /// The type of this error.
291    ///
292    /// ```
293    /// # ureq::is_test(true);
294    /// let err = ureq::get("http://httpbin.org/status/500")
295    ///     .call().unwrap_err();
296    /// assert_eq!(err.kind(), ureq::ErrorKind::HTTP);
297    /// ```
298    pub fn kind(&self) -> ErrorKind {
299        match self {
300            Error::Status(_, _) => ErrorKind::HTTP,
301            Error::Transport(Transport { kind: k, .. }) => *k,
302        }
303    }
304
305    /// Return true iff the error was due to a connection closing.
306    pub(crate) fn connection_closed(&self) -> bool {
307        if self.kind() != ErrorKind::Io {
308            return false;
309        }
310        let other_err = match self {
311            Error::Status(_, _) => return false,
312            Error::Transport(e) => e,
313        };
314        let source = match other_err.source.as_ref() {
315            Some(e) => e,
316            None => return false,
317        };
318        let ioe: &io::Error = match source.downcast_ref() {
319            Some(e) => e,
320            None => return false,
321        };
322        match ioe.kind() {
323            io::ErrorKind::ConnectionAborted => true,
324            io::ErrorKind::ConnectionReset => true,
325            _ => false,
326        }
327    }
328}
329
330/// One of the types of error the can occur when processing a Request.
331#[derive(Debug, PartialEq, Eq, Clone, Copy)]
332pub enum ErrorKind {
333    /// The url could not be understood.
334    InvalidUrl,
335    /// The url scheme could not be understood.
336    UnknownScheme,
337    /// DNS lookup failed.
338    Dns,
339    /// Insecure request attempted with https only set
340    InsecureRequestHttpsOnly,
341    /// Connection to server failed.
342    ConnectionFailed,
343    /// Too many redirects.
344    TooManyRedirects,
345    /// A status line we don't understand `HTTP/1.1 200 OK`.
346    BadStatus,
347    /// A header line that couldn't be parsed.
348    BadHeader,
349    /// Some unspecified `std::io::Error`.
350    Io,
351    /// Proxy information was not properly formatted
352    InvalidProxyUrl,
353    /// Proxy could not connect
354    ProxyConnect,
355    /// Incorrect credentials for proxy
356    ProxyUnauthorized,
357    /// HTTP status code indicating an error (e.g. 4xx, 5xx)
358    /// Read the inner response body for details and to return
359    /// the connection to the pool.
360    HTTP,
361}
362
363impl ErrorKind {
364    #[allow(clippy::wrong_self_convention)]
365    #[allow(clippy::new_ret_no_self)]
366    pub(crate) fn new(self) -> Error {
367        Error::new(self, None)
368    }
369
370    pub(crate) fn msg(self, s: impl Into<String>) -> Error {
371        Error::new(self, Some(s.into()))
372    }
373}
374
375impl From<Response> for Error {
376    fn from(resp: Response) -> Error {
377        Error::Status(resp.status(), resp)
378    }
379}
380
381impl From<io::Error> for Error {
382    fn from(err: io::Error) -> Error {
383        ErrorKind::Io.new().src(err)
384    }
385}
386
387impl From<Transport> for Error {
388    fn from(err: Transport) -> Error {
389        Error::Transport(err)
390    }
391}
392
393impl From<ParseError> for Error {
394    fn from(err: ParseError) -> Self {
395        ErrorKind::InvalidUrl
396            .msg(format!("failed to parse URL: {:?}", err))
397            .src(err)
398    }
399}
400
401impl fmt::Display for ErrorKind {
402    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
403        match self {
404            ErrorKind::InvalidUrl => write!(f, "Bad URL"),
405            ErrorKind::UnknownScheme => write!(f, "Unknown Scheme"),
406            ErrorKind::Dns => write!(f, "Dns Failed"),
407            ErrorKind::InsecureRequestHttpsOnly => {
408                write!(f, "Insecure request attempted with https_only set")
409            }
410            ErrorKind::ConnectionFailed => write!(f, "Connection Failed"),
411            ErrorKind::TooManyRedirects => write!(f, "Too Many Redirects"),
412            ErrorKind::BadStatus => write!(f, "Bad Status"),
413            ErrorKind::BadHeader => write!(f, "Bad Header"),
414            ErrorKind::Io => write!(f, "Network Error"),
415            ErrorKind::InvalidProxyUrl => write!(f, "Malformed proxy"),
416            ErrorKind::ProxyConnect => write!(f, "Proxy failed to connect"),
417            ErrorKind::ProxyUnauthorized => write!(f, "Provided proxy credentials are incorrect"),
418            ErrorKind::HTTP => write!(f, "HTTP status error"),
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn status_code_error() {
429        let mut response = Response::new(404, "NotFound", "").unwrap();
430        response.set_url("http://example.org/".parse().unwrap());
431        let err = Error::Status(response.status(), response);
432
433        assert_eq!(err.to_string(), "http://example.org/: status code 404");
434    }
435
436    #[test]
437    fn status_code_error_redirect() {
438        use crate::{get, test};
439
440        test::set_handler("/redirect_a", |unit| {
441            assert_eq!(unit.method, "GET");
442            test::make_response(
443                302,
444                "Go here",
445                vec!["Location: test://example.edu/redirect_b"],
446                vec![],
447            )
448        });
449        test::set_handler("/redirect_b", |unit| {
450            assert_eq!(unit.method, "GET");
451            test::make_response(
452                302,
453                "Go here",
454                vec!["Location: http://example.com/status/500"],
455                vec![],
456            )
457        });
458
459        let err = get("test://example.org/redirect_a").call().unwrap_err();
460        assert_eq!(err.kind(), ErrorKind::HTTP, "{:?}", err);
461        assert_eq!(
462        err.to_string(),
463        "http://example.com/status/500: status code 500 (redirected from test://example.org/redirect_a)"
464    );
465    }
466
467    #[test]
468    fn io_error() {
469        let ioe = io::Error::new(io::ErrorKind::TimedOut, "too slow");
470        let mut err = Error::new(ErrorKind::Io, Some("oops".to_string())).src(ioe);
471
472        err = err.url("http://example.com/".parse().unwrap());
473        assert_eq!(
474            err.to_string(),
475            "http://example.com/: Network Error: oops: too slow"
476        );
477    }
478
479    #[test]
480    fn connection_closed() {
481        let ioe = io::Error::new(io::ErrorKind::ConnectionReset, "connection reset");
482        let err = ErrorKind::Io.new().src(ioe);
483        assert!(err.connection_closed());
484
485        let ioe = io::Error::new(io::ErrorKind::ConnectionAborted, "connection aborted");
486        let err = ErrorKind::Io.new().src(ioe);
487        assert!(err.connection_closed());
488    }
489
490    #[test]
491    fn error_implements_send_and_sync() {
492        let _error: Box<dyn Send> = Box::new(Error::new(ErrorKind::Io, None));
493        let _error: Box<dyn Sync> = Box::new(Error::new(ErrorKind::Io, None));
494    }
495
496    #[test]
497    fn ensure_error_size() {
498        // This is platform dependent, so we can't be too strict or precise.
499        let size = std::mem::size_of::<Error>();
500        println!("Error size: {}", size);
501        assert!(size < 500); // 344 on Macbook M1
502    }
503}