1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum FetchErrorKind {
5 Timeout,
6 ConnectionRefused,
7 DnsResolution,
8 TooManyRedirects,
9 TlsError,
10 RateLimited,
11 ServerError,
12 ClientError,
13 NetworkError,
14 InvalidUrl,
15 Unknown,
16}
17
18#[derive(Debug, Clone)]
19pub struct FetchError {
20 pub kind: FetchErrorKind,
21 pub message: String,
22 pub status_code: Option<u16>,
23 pub retryable: bool,
24}
25
26impl FetchError {
27 pub fn new(kind: FetchErrorKind, message: String) -> Self {
28 let retryable = matches!(
29 kind,
30 FetchErrorKind::Timeout
31 | FetchErrorKind::ConnectionRefused
32 | FetchErrorKind::DnsResolution
33 | FetchErrorKind::RateLimited
34 | FetchErrorKind::ServerError
35 | FetchErrorKind::NetworkError
36 );
37
38 Self {
39 kind,
40 message,
41 status_code: None,
42 retryable,
43 }
44 }
45
46 pub fn with_status(mut self, status: u16) -> Self {
47 self.status_code = Some(status);
48 self
49 }
50
51 pub fn from_reqwest(err: &reqwest::Error) -> Self {
52 if err.is_timeout() {
53 return Self::new(FetchErrorKind::Timeout, err.to_string());
54 }
55
56 if err.is_connect() {
57 return Self::new(FetchErrorKind::ConnectionRefused, err.to_string());
58 }
59
60 if err.is_redirect() {
61 return Self::new(FetchErrorKind::TooManyRedirects, err.to_string());
62 }
63
64 if let Some(status) = err.status() {
65 let code = status.as_u16();
66 if code == 429 {
67 return Self::new(FetchErrorKind::RateLimited, err.to_string()).with_status(code);
68 }
69 if (500..600).contains(&code) {
70 return Self::new(FetchErrorKind::ServerError, err.to_string()).with_status(code);
71 }
72 if (400..500).contains(&code) {
73 return Self::new(FetchErrorKind::ClientError, err.to_string()).with_status(code);
74 }
75 }
76
77 Self::new(FetchErrorKind::NetworkError, err.to_string())
78 }
79
80 pub fn is_retryable(&self) -> bool {
81 self.retryable
82 }
83}
84
85impl fmt::Display for FetchError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "{:?}: {}", self.kind, self.message)?;
88 if let Some(status) = self.status_code {
89 write!(f, " (status: {})", status)?;
90 }
91 Ok(())
92 }
93}
94
95impl std::error::Error for FetchError {}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn timeout_is_retryable() {
103 let err = FetchError::new(FetchErrorKind::Timeout, "timeout".to_string());
104 assert!(err.is_retryable());
105 }
106
107 #[test]
108 fn client_error_not_retryable() {
109 let err = FetchError::new(FetchErrorKind::ClientError, "404".to_string());
110 assert!(!err.is_retryable());
111 }
112
113 #[test]
114 fn server_error_is_retryable() {
115 let err = FetchError::new(FetchErrorKind::ServerError, "500".to_string());
116 assert!(err.is_retryable());
117 }
118
119 #[test]
120 fn rate_limited_is_retryable() {
121 let err = FetchError::new(FetchErrorKind::RateLimited, "429".to_string());
122 assert!(err.is_retryable());
123 }
124}