1use std::fmt;
2use std::net::IpAddr;
3use std::time::Duration;
4
5use crate::netpolicy::{AddressClass, AddressDenial};
6
7pub const MAX_ERROR_BODY_BYTES: usize = 64 * 1024;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum Phase {
15 Connect,
17 Headers,
19 Body,
21 Redirect,
23}
24
25impl Phase {
26 pub fn as_str(self) -> &'static str {
27 match self {
28 Phase::Connect => "connect",
29 Phase::Headers => "headers",
30 Phase::Body => "body",
31 Phase::Redirect => "redirect",
32 }
33 }
34}
35
36#[derive(Debug)]
37#[non_exhaustive]
38pub enum HttpError {
39 InvalidRequest(String),
41 Transport {
46 message: String,
47 connect_phase: bool,
48 phase: Phase,
49 },
50 Timeout { message: String, phase: Phase },
52 Status {
54 status: u16,
55 body: String,
56 retry_after: Option<Duration>,
57 attempts: u32,
58 },
59 BodyTooLarge { limit: usize },
61 Json(String),
63 Stream(String),
65 Cancelled,
67 SystemProxyUnsupported { element: String },
77 AddressDenied {
82 host: String,
83 addr: Option<IpAddr>,
84 class: Option<AddressClass>,
85 reason: String,
86 },
87 HttpsRequired {
91 host: String,
92 scheme: String,
93 redirect: bool,
94 },
95}
96
97impl From<AddressDenial> for HttpError {
98 fn from(d: AddressDenial) -> Self {
99 HttpError::AddressDenied {
100 host: d.host,
101 addr: d.addr,
102 class: d.class,
103 reason: d.reason,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub enum ErrorClass {
113 Auth,
115 RateLimit { retry_after: Option<Duration> },
117 Server,
119 Client,
121 Timeout,
123 Transport,
125 Cancelled,
127 AddressDenied,
129 Invalid,
133}
134
135impl HttpError {
136 pub(crate) fn timeout(message: impl Into<String>, phase: Phase) -> Self {
137 HttpError::Timeout {
138 message: message.into(),
139 phase,
140 }
141 }
142
143 pub(crate) fn transport(message: impl Into<String>, phase: Phase) -> Self {
144 HttpError::Transport {
145 message: message.into(),
146 connect_phase: phase == Phase::Connect,
147 phase,
148 }
149 }
150
151 pub fn phase(&self) -> Option<Phase> {
153 match self {
154 HttpError::Timeout { phase, .. } | HttpError::Transport { phase, .. } => Some(*phase),
155 _ => None,
156 }
157 }
158
159 pub(crate) fn into_redirect_phase(self) -> Self {
162 match self {
163 HttpError::Timeout { message, .. } => HttpError::timeout(message, Phase::Redirect),
164 HttpError::Transport { message, .. } => HttpError::Transport {
165 message,
166 connect_phase: false,
167 phase: Phase::Redirect,
168 },
169 other => other,
170 }
171 }
172
173 pub fn status(&self) -> Option<u16> {
174 match self {
175 HttpError::Status { status, .. } => Some(*status),
176 _ => None,
177 }
178 }
179
180 pub fn class(&self) -> ErrorClass {
182 match self {
183 HttpError::Status {
184 status,
185 retry_after,
186 ..
187 } => match *status {
188 401 | 403 | 407 => ErrorClass::Auth,
189 408 => ErrorClass::Timeout,
190 429 => ErrorClass::RateLimit {
191 retry_after: *retry_after,
192 },
193 500..=599 => ErrorClass::Server,
194 _ => ErrorClass::Client,
195 },
196 HttpError::Timeout { .. } => ErrorClass::Timeout,
197 HttpError::Transport { .. } => ErrorClass::Transport,
198 HttpError::Cancelled => ErrorClass::Cancelled,
199 HttpError::SystemProxyUnsupported { .. } => ErrorClass::Invalid,
200 HttpError::AddressDenied { .. } => ErrorClass::AddressDenied,
201 HttpError::HttpsRequired { .. } => ErrorClass::Invalid,
202 HttpError::InvalidRequest(_)
203 | HttpError::Json(_)
204 | HttpError::Stream(_)
205 | HttpError::BodyTooLarge { .. } => ErrorClass::Invalid,
206 }
207 }
208}
209
210impl fmt::Display for HttpError {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 HttpError::InvalidRequest(m) => write!(f, "invalid request: {m}"),
214 HttpError::Transport { message, .. } => write!(f, "transport error: {message}"),
215 HttpError::Timeout { message, .. } => write!(f, "timeout: {message}"),
216 HttpError::Status {
217 status, attempts, ..
218 } => {
219 write!(f, "HTTP {status} after {attempts} attempt(s)")
220 }
221 HttpError::BodyTooLarge { limit } => write!(f, "response body exceeds {limit} bytes"),
222 HttpError::Json(m) => write!(f, "invalid JSON: {m}"),
223 HttpError::Stream(m) => write!(f, "stream error: {m}"),
224 HttpError::Cancelled => write!(f, "cancelled"),
225 HttpError::SystemProxyUnsupported { element } => write!(
226 f,
227 "the Windows proxy settings cannot be followed exactly by this version \
228 ({element}); nothing was sent"
229 ),
230 HttpError::AddressDenied { host, reason, .. } => {
231 write!(f, "address denied for host '{host}': {reason}")
232 }
233 HttpError::HttpsRequired {
234 host,
235 scheme,
236 redirect,
237 } => {
238 let what = if *redirect {
239 "redirect to"
240 } else {
241 "request to"
242 };
243 write!(
244 f,
245 "https required: {what} '{scheme}' URL for host '{host}' refused"
246 )
247 }
248 }
249 }
250}
251
252impl std::error::Error for HttpError {}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 fn status(code: u16, retry_after: Option<Duration>) -> HttpError {
259 HttpError::Status {
260 status: code,
261 body: String::new(),
262 retry_after,
263 attempts: 1,
264 }
265 }
266
267 #[test]
268 fn statuses_map_to_their_classes() {
269 for code in [401, 403, 407] {
270 assert_eq!(status(code, None).class(), ErrorClass::Auth, "{code}");
271 }
272 for code in [500, 502, 503, 504, 599] {
273 assert_eq!(status(code, None).class(), ErrorClass::Server, "{code}");
274 }
275 for code in [400, 402, 404, 409, 410, 422, 451, 499, 301, 302] {
276 assert_eq!(status(code, None).class(), ErrorClass::Client, "{code}");
277 }
278 }
279
280 #[test]
281 fn http_408_is_a_timeout_not_a_client_error() {
282 assert_eq!(status(408, None).class(), ErrorClass::Timeout);
283 assert_eq!(status(408, None).status(), Some(408));
284 }
285
286 #[test]
287 fn rate_limit_carries_the_parsed_retry_after() {
288 assert_eq!(
289 status(429, Some(Duration::from_secs(7))).class(),
290 ErrorClass::RateLimit {
291 retry_after: Some(Duration::from_secs(7))
292 }
293 );
294 assert_eq!(
295 status(429, None).class(),
296 ErrorClass::RateLimit { retry_after: None }
297 );
298 }
299
300 #[test]
301 fn non_status_errors_map_to_their_classes() {
302 assert_eq!(
303 HttpError::timeout("x", Phase::Body).class(),
304 ErrorClass::Timeout
305 );
306 for phase in [Phase::Connect, Phase::Headers, Phase::Body, Phase::Redirect] {
307 assert_eq!(
308 HttpError::transport("x", phase).class(),
309 ErrorClass::Transport
310 );
311 }
312 assert_eq!(HttpError::Cancelled.class(), ErrorClass::Cancelled);
313 assert_eq!(
314 HttpError::AddressDenied {
315 host: "h".into(),
316 addr: None,
317 class: None,
318 reason: "r".into()
319 }
320 .class(),
321 ErrorClass::AddressDenied
322 );
323 assert_eq!(
324 HttpError::InvalidRequest("x".into()).class(),
325 ErrorClass::Invalid
326 );
327 assert_eq!(HttpError::Json("x".into()).class(), ErrorClass::Invalid);
328 assert_eq!(HttpError::Stream("x".into()).class(), ErrorClass::Invalid);
329 assert_eq!(
330 HttpError::BodyTooLarge { limit: 1 }.class(),
331 ErrorClass::Invalid
332 );
333 }
334
335 #[test]
336 fn phase_is_carried_by_timeout_and_transport_only() {
337 for phase in [Phase::Connect, Phase::Headers, Phase::Body, Phase::Redirect] {
338 assert_eq!(HttpError::timeout("t", phase).phase(), Some(phase));
339 assert_eq!(HttpError::transport("t", phase).phase(), Some(phase));
340 }
341 assert_eq!(HttpError::Cancelled.phase(), None);
342 assert_eq!(status(500, None).phase(), None);
343 assert_eq!(HttpError::InvalidRequest("x".into()).phase(), None);
344 }
345
346 #[test]
347 fn connect_phase_is_true_exactly_for_the_connect_phase() {
348 for (phase, want) in [
349 (Phase::Connect, true),
350 (Phase::Headers, false),
351 (Phase::Body, false),
352 (Phase::Redirect, false),
353 ] {
354 match HttpError::transport("t", phase) {
355 HttpError::Transport { connect_phase, .. } => assert_eq!(connect_phase, want),
356 other => panic!("{other:?}"),
357 }
358 }
359 }
360
361 #[test]
362 fn redirect_relabel_clears_connect_phase() {
363 match HttpError::transport("t", Phase::Connect).into_redirect_phase() {
364 HttpError::Transport {
365 connect_phase,
366 phase,
367 ..
368 } => {
369 assert!(!connect_phase);
370 assert_eq!(phase, Phase::Redirect);
371 }
372 other => panic!("{other:?}"),
373 }
374 assert_eq!(
375 HttpError::timeout("t", Phase::Headers)
376 .into_redirect_phase()
377 .phase(),
378 Some(Phase::Redirect)
379 );
380 assert!(matches!(
381 HttpError::Cancelled.into_redirect_phase(),
382 HttpError::Cancelled
383 ));
384 }
385
386 #[test]
387 fn phase_names() {
388 assert_eq!(Phase::Connect.as_str(), "connect");
389 assert_eq!(Phase::Headers.as_str(), "headers");
390 assert_eq!(Phase::Body.as_str(), "body");
391 assert_eq!(Phase::Redirect.as_str(), "redirect");
392 }
393}