use crate::graphql::GraphQlError;
use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("config error: {0}")]
Config(String),
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("url error: {0}")]
Url(#[from] url::ParseError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("graphql error: {message}")]
GraphQl {
status: Option<u16>,
errors: Vec<GraphQlError>,
body: String,
message: String,
},
}
impl Error {
pub fn is_auth_error(&self) -> bool {
matches!(
self,
Error::GraphQl {
status: Some(401 | 403),
..
}
) || matches!(self, Error::Http(err) if err.status() == Some(reqwest::StatusCode::UNAUTHORIZED))
}
pub fn is_retryable(&self) -> bool {
match self {
Error::Config(_) | Error::Url(_) | Error::Json(_) => false,
Error::Http(err) => {
if err.is_timeout() || err.is_connect() {
return true;
}
match err.status() {
Some(status) => {
status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
}
None => true,
}
}
Error::GraphQl { status, .. } => {
matches!(status, Some(429 | 500 | 502 | 503 | 504))
}
}
}
}
impl fmt::Display for GraphQlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_auth_error() {
let err = Error::GraphQl {
status: Some(401),
errors: vec![],
body: String::new(),
message: "unauthorized".to_string(),
};
assert!(err.is_auth_error());
let err = Error::GraphQl {
status: Some(403),
errors: vec![],
body: String::new(),
message: "forbidden".to_string(),
};
assert!(err.is_auth_error());
let err = Error::GraphQl {
status: Some(500),
errors: vec![],
body: String::new(),
message: "server error".to_string(),
};
assert!(!err.is_auth_error());
}
#[test]
fn test_non_graphql_errors_not_auth() {
let config_err = Error::Config("bad".into());
assert!(!config_err.is_auth_error());
let url_err: Error = url::Url::parse(":::").unwrap_err().into();
assert!(!url_err.is_auth_error());
let json_err: Error = serde_json::from_str::<serde_json::Value>("!!!")
.unwrap_err()
.into();
assert!(!json_err.is_auth_error());
}
#[test]
fn test_is_retryable_server_errors() {
for status in [429, 500, 502, 503, 504] {
let err = Error::GraphQl {
status: Some(status),
errors: vec![],
body: String::new(),
message: "server error".to_string(),
};
assert!(err.is_retryable(), "status {status} should be retryable");
}
}
#[test]
fn test_is_not_retryable_client_errors() {
for status in [400, 401, 403, 404, 422] {
let err = Error::GraphQl {
status: Some(status),
errors: vec![],
body: String::new(),
message: "client error".to_string(),
};
assert!(
!err.is_retryable(),
"status {status} should not be retryable"
);
}
}
#[test]
fn test_is_not_retryable_config_url_json() {
let config_err = Error::Config("bad".into());
assert!(!config_err.is_retryable());
let url_err: Error = url::Url::parse(":::").unwrap_err().into();
assert!(!url_err.is_retryable());
let json_err: Error = serde_json::from_str::<serde_json::Value>("!!!")
.unwrap_err()
.into();
assert!(!json_err.is_retryable());
}
}