Skip to main content

argus_fetcher/
error.rs

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}