Skip to main content

nano_get/
errors.rs

1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3use std::io;
4use std::str::Utf8Error;
5
6/// Error type for all fallible operations in this crate.
7#[derive(Debug)]
8pub enum NanoGetError {
9    /// URL input was invalid.
10    InvalidUrl(String),
11    /// URL scheme is unsupported.
12    UnsupportedScheme(String),
13    /// Proxy URL scheme is unsupported.
14    UnsupportedProxyScheme(String),
15    /// HTTPS was requested without enabling the `https` feature.
16    HttpsFeatureRequired,
17    /// Header name failed validation.
18    InvalidHeaderName(String),
19    /// Header value failed validation.
20    InvalidHeaderValue(String),
21    /// TCP connect operation failed.
22    Connect(io::Error),
23    /// Generic I/O failure.
24    Io(io::Error),
25    /// TLS handshake or TLS configuration failure.
26    Tls(String),
27    /// HTTP `CONNECT` tunnel setup failed with `(status_code, reason_phrase)`.
28    ProxyConnectFailed(u16, String),
29    /// Authentication challenge syntax was malformed.
30    MalformedChallenge(String),
31    /// HTTP status line syntax was malformed.
32    MalformedStatusLine(String),
33    /// Header block syntax was malformed.
34    MalformedHeader(String),
35    /// `Content-Length` was invalid or conflicting.
36    InvalidContentLength(String),
37    /// Chunked transfer framing was invalid.
38    InvalidChunk(String),
39    /// Transfer encoding is unsupported by this crate.
40    UnsupportedTransferEncoding(String),
41    /// Response used ambiguous body framing.
42    AmbiguousResponseFraming(String),
43    /// Response body ended before the declared framing boundary.
44    IncompleteMessage(String),
45    /// Redirect chain exceeded the configured maximum.
46    RedirectLimitExceeded(usize),
47    /// Response body could not be decoded as UTF-8.
48    InvalidUtf8(Utf8Error),
49    /// In-memory cache operation failed.
50    Cache(String),
51    /// Pipelining operation failed.
52    Pipeline(String),
53    /// Generic authentication error.
54    Authentication(String),
55    /// Authentication retries would loop.
56    AuthenticationLoop(String),
57    /// Authentication handler explicitly rejected continuation.
58    AuthenticationRejected(String),
59    /// Caller attempted to set a protocol-managed header.
60    ProtocolManagedHeader(String),
61    /// Caller attempted to set a forbidden hop-by-hop header.
62    HopByHopHeader(String),
63    /// Conditional request header combination was invalid.
64    InvalidConditionalRequest(String),
65}
66
67impl NanoGetError {
68    pub(crate) fn invalid_url(message: impl Into<String>) -> Self {
69        Self::InvalidUrl(message.into())
70    }
71}
72
73impl Display for NanoGetError {
74    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::InvalidUrl(message) => write!(f, "invalid URL: {message}"),
77            Self::UnsupportedScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"),
78            Self::UnsupportedProxyScheme(scheme) => {
79                write!(f, "unsupported proxy URL scheme: {scheme}")
80            }
81            Self::HttpsFeatureRequired => {
82                write!(f, "the `https` feature flag is required for HTTPS requests")
83            }
84            Self::InvalidHeaderName(name) => write!(f, "invalid header name: {name}"),
85            Self::InvalidHeaderValue(value) => write!(f, "invalid header value: {value}"),
86            Self::Connect(error) => write!(f, "failed to connect: {error}"),
87            Self::Io(error) => write!(f, "I/O error: {error}"),
88            Self::Tls(message) => write!(f, "TLS error: {message}"),
89            Self::ProxyConnectFailed(code, reason) => {
90                write!(f, "proxy CONNECT failed with status {code}: {reason}")
91            }
92            Self::MalformedChallenge(value) => {
93                write!(f, "malformed authentication challenge: {value}")
94            }
95            Self::MalformedStatusLine(line) => write!(f, "malformed status line: {line}"),
96            Self::MalformedHeader(line) => write!(f, "malformed header: {line}"),
97            Self::InvalidContentLength(value) => write!(f, "invalid content-length: {value}"),
98            Self::InvalidChunk(message) => write!(f, "invalid chunked body: {message}"),
99            Self::UnsupportedTransferEncoding(value) => {
100                write!(f, "unsupported transfer-encoding: {value}")
101            }
102            Self::AmbiguousResponseFraming(message) => {
103                write!(f, "ambiguous response framing: {message}")
104            }
105            Self::IncompleteMessage(message) => write!(f, "incomplete message: {message}"),
106            Self::RedirectLimitExceeded(limit) => {
107                write!(f, "redirect limit exceeded after {limit} hops")
108            }
109            Self::InvalidUtf8(error) => write!(f, "response body is not valid UTF-8: {error}"),
110            Self::Cache(message) => write!(f, "cache error: {message}"),
111            Self::Pipeline(message) => write!(f, "pipeline error: {message}"),
112            Self::Authentication(message) => write!(f, "authentication error: {message}"),
113            Self::AuthenticationLoop(message) => {
114                write!(f, "authentication retry loop detected: {message}")
115            }
116            Self::AuthenticationRejected(message) => {
117                write!(f, "authentication rejected: {message}")
118            }
119            Self::ProtocolManagedHeader(name) => {
120                write!(
121                    f,
122                    "header is managed by the protocol implementation: {name}"
123                )
124            }
125            Self::HopByHopHeader(name) => write!(f, "hop-by-hop header is not allowed: {name}"),
126            Self::InvalidConditionalRequest(message) => {
127                write!(f, "invalid conditional request: {message}")
128            }
129        }
130    }
131}
132
133impl Error for NanoGetError {
134    fn source(&self) -> Option<&(dyn Error + 'static)> {
135        match self {
136            Self::Connect(error) | Self::Io(error) => Some(error),
137            Self::InvalidUtf8(error) => Some(error),
138            _ => None,
139        }
140    }
141}
142
143impl From<io::Error> for NanoGetError {
144    fn from(error: io::Error) -> Self {
145        Self::Io(error)
146    }
147}
148
149impl From<Utf8Error> for NanoGetError {
150    fn from(error: Utf8Error) -> Self {
151        Self::InvalidUtf8(error)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use std::error::Error as _;
158    use std::io;
159
160    use super::NanoGetError;
161
162    #[test]
163    fn formats_new_error_variants() {
164        let ambiguous = NanoGetError::AmbiguousResponseFraming("bad".to_string());
165        assert_eq!(ambiguous.to_string(), "ambiguous response framing: bad");
166
167        let incomplete = NanoGetError::IncompleteMessage("eof".to_string());
168        assert_eq!(incomplete.to_string(), "incomplete message: eof");
169
170        let conditional = NanoGetError::InvalidConditionalRequest("invalid".to_string());
171        assert_eq!(
172            conditional.to_string(),
173            "invalid conditional request: invalid"
174        );
175    }
176
177    #[test]
178    fn formats_all_error_variants_and_sources() {
179        let invalid = vec![0xff];
180        let utf8_error = std::str::from_utf8(&invalid).unwrap_err();
181        let io_error = io::Error::new(io::ErrorKind::Other, "io");
182        let connect_error = io::Error::new(io::ErrorKind::ConnectionRefused, "connect");
183
184        let variants = vec![
185            NanoGetError::InvalidUrl("url".to_string()),
186            NanoGetError::UnsupportedScheme("ftp".to_string()),
187            NanoGetError::UnsupportedProxyScheme("https".to_string()),
188            NanoGetError::HttpsFeatureRequired,
189            NanoGetError::InvalidHeaderName("x".to_string()),
190            NanoGetError::InvalidHeaderValue("y".to_string()),
191            NanoGetError::Connect(connect_error),
192            NanoGetError::Io(io_error),
193            NanoGetError::Tls("tls".to_string()),
194            NanoGetError::ProxyConnectFailed(407, "Proxy".to_string()),
195            NanoGetError::MalformedChallenge("challenge".to_string()),
196            NanoGetError::MalformedStatusLine("status".to_string()),
197            NanoGetError::MalformedHeader("header".to_string()),
198            NanoGetError::InvalidContentLength("len".to_string()),
199            NanoGetError::InvalidChunk("chunk".to_string()),
200            NanoGetError::UnsupportedTransferEncoding("gzip".to_string()),
201            NanoGetError::AmbiguousResponseFraming("ambiguous".to_string()),
202            NanoGetError::IncompleteMessage("incomplete".to_string()),
203            NanoGetError::RedirectLimitExceeded(3),
204            NanoGetError::InvalidUtf8(utf8_error),
205            NanoGetError::Cache("cache".to_string()),
206            NanoGetError::Pipeline("pipeline".to_string()),
207            NanoGetError::Authentication("auth".to_string()),
208            NanoGetError::AuthenticationLoop("loop".to_string()),
209            NanoGetError::AuthenticationRejected("rejected".to_string()),
210            NanoGetError::ProtocolManagedHeader("host".to_string()),
211            NanoGetError::HopByHopHeader("te".to_string()),
212            NanoGetError::InvalidConditionalRequest("conditional".to_string()),
213        ];
214
215        for error in variants {
216            assert!(!error.to_string().is_empty());
217        }
218
219        let io_error = NanoGetError::from(io::Error::new(io::ErrorKind::Other, "io"));
220        assert!(io_error.source().is_some());
221        let invalid = vec![0xff];
222        let utf8_error = NanoGetError::from(std::str::from_utf8(&invalid).unwrap_err());
223        assert!(utf8_error.source().is_some());
224        assert!(NanoGetError::UnsupportedScheme("ftp".to_string())
225            .source()
226            .is_none());
227    }
228}