use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FaucetError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("HTTP {status} from {url}: {body}")]
HttpStatus {
status: u16,
url: String,
body: String,
},
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("JSONPath error: {0}")]
JsonPath(String),
#[error("Auth error: {0}")]
Auth(String),
#[error("Rate limited: retry after {0:?}")]
RateLimited(Duration),
#[error("URL error: {0}")]
Url(String),
#[error("Transform error: {0}")]
Transform(String),
}
impl FaucetError {
pub fn is_retriable(&self) -> bool {
match self {
FaucetError::Http(e) => {
if let Some(status) = e.status() {
status.is_server_error()
} else {
true
}
}
FaucetError::HttpStatus { status, .. } => *status >= 500,
FaucetError::RateLimited(_) => true,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_status_5xx_is_retriable() {
let err = FaucetError::HttpStatus {
status: 500,
url: "https://example.com".into(),
body: "Internal Server Error".into(),
};
assert!(err.is_retriable());
let err = FaucetError::HttpStatus {
status: 503,
url: "https://example.com".into(),
body: "".into(),
};
assert!(err.is_retriable());
}
#[test]
fn http_status_4xx_is_not_retriable() {
let err = FaucetError::HttpStatus {
status: 400,
url: "https://example.com".into(),
body: "Bad Request".into(),
};
assert!(!err.is_retriable());
let err = FaucetError::HttpStatus {
status: 404,
url: "https://example.com".into(),
body: "".into(),
};
assert!(!err.is_retriable());
}
#[test]
fn rate_limited_is_retriable() {
let err = FaucetError::RateLimited(Duration::from_secs(30));
assert!(err.is_retriable());
}
#[test]
fn json_error_is_not_retriable() {
let serde_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
let err = FaucetError::Json(serde_err);
assert!(!err.is_retriable());
}
#[test]
fn jsonpath_error_is_not_retriable() {
let err = FaucetError::JsonPath("bad path".into());
assert!(!err.is_retriable());
}
#[test]
fn auth_error_is_not_retriable() {
let err = FaucetError::Auth("invalid token".into());
assert!(!err.is_retriable());
}
#[test]
fn url_error_is_not_retriable() {
let err = FaucetError::Url("bad url".into());
assert!(!err.is_retriable());
}
#[test]
fn transform_error_is_not_retriable() {
let err = FaucetError::Transform("bad regex".into());
assert!(!err.is_retriable());
}
#[test]
fn http_status_display_includes_url_and_body() {
let err = FaucetError::HttpStatus {
status: 422,
url: "https://api.example.com/test".into(),
body: "Unprocessable Entity".into(),
};
let msg = err.to_string();
assert!(msg.contains("422"));
assert!(msg.contains("https://api.example.com/test"));
assert!(msg.contains("Unprocessable Entity"));
}
}