reqwest_rotate/error.rs
1//! The error type returned by this crate.
2
3use thiserror::Error;
4
5/// Errors produced by [`RotatingClient`](crate::RotatingClient) and its
6/// builder.
7///
8/// HTTP status codes are never errors: after the last attempt the response
9/// is handed back as-is, whatever its status, exactly like `reqwest` does.
10/// Use [`Response::error_for_status`](reqwest::Response::error_for_status)
11/// if you want a non-2xx status to become an error.
12///
13/// Where a variant wraps a `reqwest::Error`, that error is this one's
14/// [`source`](std::error::Error::source); [`Reqwest`](Error::Reqwest) and
15/// [`Build`](Error::Build) do not repeat it in their own message.
16/// [`InvalidProxy`](Error::InvalidProxy) is the exception: its message
17/// includes the reason for readability even though the same text is also
18/// reachable through `source`.
19///
20/// Marked `#[non_exhaustive]`: new variants may be added in a minor release
21/// without that counting as a breaking change.
22#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum Error {
25 /// A request failed at the `reqwest` layer (network error, timeout,
26 /// invalid URL, ...). Returned directly for failures that are not
27 /// retried, and as the last attempt's failure once retries are used up
28 /// on a transient transport error.
29 #[error("request failed")]
30 Reqwest(#[from] reqwest::Error),
31
32 /// A proxy URL is blank, cannot be parsed, or uses a scheme this build
33 /// does not support (SOCKS schemes need the `socks` feature); or a
34 /// syntactically valid proxy was rejected when the underlying
35 /// `reqwest::Client` was built.
36 ///
37 /// [`source`](std::error::Error::source) carries the reason: either a
38 /// plain-text explanation with no cause of its own, or the
39 /// `reqwest::Error` from the failed client build.
40 #[error("invalid proxy: {proxy}: {source}")]
41 InvalidProxy {
42 /// The proxy URL as far as it could be read, or the caller's own
43 /// spelling if it could not be parsed at all. Credentials are
44 /// always redacted.
45 proxy: String,
46 /// Why the proxy was rejected.
47 #[source]
48 source: Box<dyn std::error::Error + Send + Sync + 'static>,
49 },
50
51 /// The underlying `reqwest::Client` could not be built: an invalid TLS
52 /// or proxy configuration, usually.
53 #[error("failed to build the underlying client")]
54 Build(#[source] reqwest::Error),
55}
56
57impl Error {
58 /// Builds [`Error::InvalidProxy`] from a redacted proxy string and a
59 /// plain-text reason, for a validation failure that has no underlying
60 /// error of its own.
61 pub(crate) fn invalid_proxy(proxy: impl Into<String>, reason: impl Into<String>) -> Self {
62 Error::InvalidProxy {
63 proxy: proxy.into(),
64 source: Box::new(ProxyReason(reason.into())),
65 }
66 }
67}
68
69/// A plain-text reason for rejecting a proxy URL, with no underlying cause
70/// of its own.
71#[derive(Debug)]
72struct ProxyReason(String);
73
74impl std::fmt::Display for ProxyReason {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str(&self.0)
77 }
78}
79
80impl std::error::Error for ProxyReason {}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn build_error_keeps_its_source() {
88 let inner = reqwest::Proxy::all("http://[").unwrap_err();
89 let err = Error::Build(inner);
90 assert!(std::error::Error::source(&err).is_some());
91 assert_eq!(err.to_string(), "failed to build the underlying client");
92 }
93
94 #[test]
95 fn reqwest_display_is_bare() {
96 let inner = reqwest::Proxy::all("http://[").unwrap_err();
97 let err = Error::from(inner);
98 assert!(std::error::Error::source(&err).is_some());
99 assert_eq!(err.to_string(), "request failed");
100 }
101
102 #[test]
103 fn invalid_proxy_keeps_its_source() {
104 let err = Error::invalid_proxy("http://proxy.example", "not a valid proxy URL");
105 assert!(std::error::Error::source(&err).is_some());
106 assert_eq!(
107 err.to_string(),
108 "invalid proxy: http://proxy.example: not a valid proxy URL"
109 );
110 }
111}