1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3use std::io;
4use std::str::Utf8Error;
5
6#[derive(Debug)]
8pub enum NanoGetError {
9 InvalidUrl(String),
11 UnsupportedScheme(String),
13 UnsupportedProxyScheme(String),
15 HttpsFeatureRequired,
17 InvalidHeaderName(String),
19 InvalidHeaderValue(String),
21 Connect(io::Error),
23 Io(io::Error),
25 Tls(String),
27 ProxyConnectFailed(u16, String),
29 MalformedChallenge(String),
31 MalformedStatusLine(String),
33 MalformedHeader(String),
35 InvalidContentLength(String),
37 InvalidChunk(String),
39 UnsupportedTransferEncoding(String),
41 AmbiguousResponseFraming(String),
43 IncompleteMessage(String),
45 RedirectLimitExceeded(usize),
47 InvalidUtf8(Utf8Error),
49 Cache(String),
51 Pipeline(String),
53 Authentication(String),
55 AuthenticationLoop(String),
57 AuthenticationRejected(String),
59 ProtocolManagedHeader(String),
61 HopByHopHeader(String),
63 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}