1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum FaucetError {
9 #[error("HTTP error: {0}")]
10 Http(#[from] reqwest::Error),
11
12 #[error("HTTP {status} from {url}: {body}")]
18 HttpStatus {
19 status: u16,
20 url: String,
21 body: String,
22 },
23
24 #[error("JSON error: {0}")]
25 Json(#[from] serde_json::Error),
26
27 #[error("JSONPath error: {0}")]
28 JsonPath(String),
29
30 #[error("Auth error: {0}")]
31 Auth(String),
32
33 #[error("Rate limited: retry after {0:?}")]
37 RateLimited(Duration),
38
39 #[error("URL error: {0}")]
41 Url(String),
42
43 #[error("Transform error: {0}")]
45 Transform(String),
46}
47
48impl FaucetError {
49 pub fn is_retriable(&self) -> bool {
60 match self {
61 FaucetError::Http(e) => {
63 if let Some(status) = e.status() {
65 status.is_server_error()
66 } else {
67 true
69 }
70 }
71 FaucetError::HttpStatus { status, .. } => *status >= 500,
72 FaucetError::RateLimited(_) => true,
73 _ => false,
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn http_status_5xx_is_retriable() {
84 let err = FaucetError::HttpStatus {
85 status: 500,
86 url: "https://example.com".into(),
87 body: "Internal Server Error".into(),
88 };
89 assert!(err.is_retriable());
90
91 let err = FaucetError::HttpStatus {
92 status: 503,
93 url: "https://example.com".into(),
94 body: "".into(),
95 };
96 assert!(err.is_retriable());
97 }
98
99 #[test]
100 fn http_status_4xx_is_not_retriable() {
101 let err = FaucetError::HttpStatus {
102 status: 400,
103 url: "https://example.com".into(),
104 body: "Bad Request".into(),
105 };
106 assert!(!err.is_retriable());
107
108 let err = FaucetError::HttpStatus {
109 status: 404,
110 url: "https://example.com".into(),
111 body: "".into(),
112 };
113 assert!(!err.is_retriable());
114 }
115
116 #[test]
117 fn rate_limited_is_retriable() {
118 let err = FaucetError::RateLimited(Duration::from_secs(30));
119 assert!(err.is_retriable());
120 }
121
122 #[test]
123 fn json_error_is_not_retriable() {
124 let serde_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
125 let err = FaucetError::Json(serde_err);
126 assert!(!err.is_retriable());
127 }
128
129 #[test]
130 fn jsonpath_error_is_not_retriable() {
131 let err = FaucetError::JsonPath("bad path".into());
132 assert!(!err.is_retriable());
133 }
134
135 #[test]
136 fn auth_error_is_not_retriable() {
137 let err = FaucetError::Auth("invalid token".into());
138 assert!(!err.is_retriable());
139 }
140
141 #[test]
142 fn url_error_is_not_retriable() {
143 let err = FaucetError::Url("bad url".into());
144 assert!(!err.is_retriable());
145 }
146
147 #[test]
148 fn transform_error_is_not_retriable() {
149 let err = FaucetError::Transform("bad regex".into());
150 assert!(!err.is_retriable());
151 }
152
153 #[test]
154 fn http_status_display_includes_url_and_body() {
155 let err = FaucetError::HttpStatus {
156 status: 422,
157 url: "https://api.example.com/test".into(),
158 body: "Unprocessable Entity".into(),
159 };
160 let msg = err.to_string();
161 assert!(msg.contains("422"));
162 assert!(msg.contains("https://api.example.com/test"));
163 assert!(msg.contains("Unprocessable Entity"));
164 }
165}