use std::fmt;
use std::time::Duration;
use crate::download::DownloadError;
use crate::pagination::PaginationError;
use crate::policy::InsecureTransportError;
use crate::stream::StreamProtocolError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Api(Box<ApiError>),
#[error("not modified")]
NotModified {
etag: Option<String>,
},
#[error(transparent)]
InsecureTransport(#[from] InsecureTransportError),
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error(transparent)]
Decode(Box<DecodeError>),
#[error(transparent)]
Stream(#[from] StreamProtocolError),
#[error(transparent)]
Download(#[from] DownloadError),
#[error(transparent)]
Pagination(#[from] PaginationError),
#[error("invalid configuration: {0}")]
Config(String),
}
impl Error {
pub fn as_api(&self) -> Option<&ApiError> {
match self {
Self::Api(error) => Some(error),
_ => None,
}
}
pub fn status(&self) -> Option<u16> {
match self {
Self::Api(error) => Some(error.status),
Self::NotModified { .. } => Some(304),
_ => None,
}
}
pub fn is_not_modified(&self) -> bool {
matches!(self, Self::NotModified { .. })
}
}
impl From<ApiError> for Error {
fn from(error: ApiError) -> Self {
Self::Api(Box::new(error))
}
}
impl From<DecodeError> for Error {
fn from(error: DecodeError) -> Self {
Self::Decode(Box::new(error))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ApiErrorKind {
BadRequest,
Authentication,
SubscriptionRequired,
PermissionDenied,
NotFound,
RequestTimeout,
Conflict,
Unprocessable,
AccountLocked,
RateLimited,
Server,
Other,
}
impl ApiErrorKind {
pub fn from_status(status: u16) -> Self {
match status {
400 => Self::BadRequest,
401 => Self::Authentication,
402 => Self::SubscriptionRequired,
403 => Self::PermissionDenied,
404 => Self::NotFound,
408 => Self::RequestTimeout,
409 => Self::Conflict,
422 => Self::Unprocessable,
423 => Self::AccountLocked,
429 => Self::RateLimited,
500..=599 => Self::Server,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ApiError {
pub status: u16,
pub code: Option<String>,
pub reason: Option<String>,
pub message: String,
pub param: Option<String>,
pub doc_url: Option<String>,
pub retry_at: Option<String>,
pub retry_after: Option<Duration>,
pub request_id: Option<String>,
pub body: Option<serde_json::Value>,
}
impl ApiError {
pub fn kind(&self) -> ApiErrorKind {
ApiErrorKind::from_status(self.status)
}
pub fn is_rate_limited(&self) -> bool {
self.status == 429
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HTTP {} {}", self.status, self.code.as_deref().unwrap_or("error"))?;
if let Some(reason) = &self.reason {
write!(f, " ({reason})")?;
}
write!(f, ": {}", self.message)?;
if let Some(request_id) = &self.request_id {
write!(f, " [request {request_id}]")?;
}
Ok(())
}
}
impl std::error::Error for ApiError {}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct DecodeError {
pub operation: String,
pub status: u16,
pub path: String,
pub message: String,
pub body: String,
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} answered HTTP {} with a body this release cannot decode at `{}`: {}",
self.operation, self.status, self.path, self.message
)
}
}
impl std::error::Error for DecodeError {}