use std::fmt;
use std::time::Duration;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("bad request: {0}")]
BadRequest(ApiError),
#[error("authentication failed: {0}")]
Authentication(ApiError),
#[error("payment required: {0}")]
PaymentRequired(ApiError),
#[error("payload too large: {0}")]
PayloadTooLarge(ApiError),
#[error("unsupported encoding: {0}")]
UnsupportedEncoding(ApiError),
#[error("rate limited: {0}")]
RateLimited(ApiError),
#[error("server overloaded: {0}")]
Overloaded(ApiError),
#[error("API error: {0}")]
Api(ApiError),
#[error("connection error: {0}")]
Connection(#[source] BoxError),
#[error("request timed out: {0}")]
Timeout(#[source] BoxError),
#[error("invalid argument: {0}")]
InvalidArgument(String),
}
impl Error {
pub fn api_error(&self) -> Option<&ApiError> {
match self {
Error::BadRequest(e)
| Error::Authentication(e)
| Error::PaymentRequired(e)
| Error::PayloadTooLarge(e)
| Error::UnsupportedEncoding(e)
| Error::RateLimited(e)
| Error::Overloaded(e)
| Error::Api(e) => Some(e),
_ => None,
}
}
pub fn status(&self) -> Option<u16> {
self.api_error().map(|e| e.status)
}
pub fn retry_after(&self) -> Option<Duration> {
self.api_error().and_then(|e| e.retry_after)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::RateLimited(_) | Error::Overloaded(_) | Error::Connection(_)
)
}
pub(crate) fn from_status(
status: u16,
message: String,
retry_after: Option<Duration>,
) -> Error {
let e = ApiError {
status,
message,
retry_after,
};
match status {
400 => Error::BadRequest(e),
401 => Error::Authentication(e),
402 => Error::PaymentRequired(e),
413 => Error::PayloadTooLarge(e),
415 => Error::UnsupportedEncoding(e),
429 => Error::RateLimited(e),
503 => Error::Overloaded(e),
_ => Error::Api(e),
}
}
pub(crate) fn from_transport(e: reqwest::Error) -> Error {
if e.is_timeout() {
Error::Timeout(Box::new(e))
} else {
Error::Connection(Box::new(e))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ApiError {
pub status: u16,
pub message: String,
pub retry_after: Option<Duration>,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HTTP {}", self.status)?;
if !self.message.is_empty() {
write!(f, ": {}", self.message)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_statuses() {
type Check = fn(&Error) -> bool;
let cases: [(u16, Check); 9] = [
(400, |e| matches!(e, Error::BadRequest(_))),
(401, |e| matches!(e, Error::Authentication(_))),
(402, |e| matches!(e, Error::PaymentRequired(_))),
(413, |e| matches!(e, Error::PayloadTooLarge(_))),
(415, |e| matches!(e, Error::UnsupportedEncoding(_))),
(429, |e| matches!(e, Error::RateLimited(_))),
(503, |e| matches!(e, Error::Overloaded(_))),
(500, |e| matches!(e, Error::Api(_))),
(404, |e| matches!(e, Error::Api(_))),
];
for (status, check) in cases {
let e = Error::from_status(status, "msg".into(), None);
assert!(check(&e), "{status} mapped to {e:?}");
assert_eq!(e.status(), Some(status));
}
}
#[test]
fn display_includes_status_and_message() {
let e = Error::from_status(400, "parse error: x".into(), None);
assert_eq!(e.to_string(), "bad request: HTTP 400: parse error: x");
}
}