use http_kit::{BodyError, Response, StatusCode};
use std::error::Error as StdError;
use thiserror::Error;
#[derive(Debug, Error)]
#[allow(clippy::large_enum_variant)]
pub enum Error {
#[error("HTTP error {status}: {message}")]
Http {
status: StatusCode,
message: String,
#[source]
response: Box<HttpErrorResponse>,
},
#[error("transport error: {0}")]
Transport(#[source] Box<dyn StdError + Send + Sync>),
#[error("TLS error: {0}")]
Tls(#[source] Box<dyn StdError + Send + Sync>),
#[error("request timed out")]
Timeout,
#[error("too many redirects (max {max})")]
TooManyRedirects {
max: u32,
},
#[error("invalid redirect location")]
InvalidRedirectLocation,
#[error("invalid URI: {0}")]
InvalidUri(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("failed to parse response body: {0}")]
BodyParse(#[from] BodyError),
#[error("cookie error: {0}")]
Cookie(#[from] CookieErrorKind),
#[error("OAuth2 error: {0}")]
OAuth2(#[from] OAuth2ErrorKind),
#[error("download error: {0}")]
Download(#[from] DownloadErrorKind),
#[error("websocket error: {0}")]
WebSocket(#[from] WebSocketErrorKind),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("other error: {0}")]
Other(#[source] Box<dyn StdError + Send + Sync>),
}
#[derive(Debug)]
pub struct HttpErrorResponse {
pub response: Response,
pub body_text: Option<String>,
}
impl StdError for HttpErrorResponse {}
impl std::fmt::Display for HttpErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HTTP response with status {}", self.response.status())
}
}
#[derive(Debug, Error)]
pub enum CookieErrorKind {
#[error("failed to load cookies from disk: {0}")]
LoadFailed(#[source] std::io::Error),
#[error("failed to parse cookies: {0}")]
ParseFailed(#[source] serde_json::Error),
#[error("failed to persist cookies: {0}")]
PersistFailed(#[source] std::io::Error),
#[error("invalid cookie header")]
InvalidHeader,
}
#[derive(Debug, Error)]
pub enum OAuth2ErrorKind {
#[error("failed to fetch token: {0}")]
TokenFetchFailed(String),
#[error("token endpoint returned error: {status} - {message}")]
TokenEndpointError {
status: StatusCode,
message: String,
},
#[error("invalid token response: {0}")]
InvalidTokenResponse(String),
}
#[derive(Debug, Error)]
pub enum DownloadErrorKind {
#[error("upstream returned error: {0}")]
UpstreamError(StatusCode),
#[error("file system error: {0}")]
FileSystem(#[source] std::io::Error),
#[error("failed to read response body: {0}")]
BodyRead(String),
}
#[derive(Debug, Error)]
pub enum WebSocketErrorKind {
#[error("failed to encode payload: {0}")]
EncodeFailed(#[source] serde_json::Error),
#[error("unsupported scheme: {0}")]
UnsupportedScheme(String),
#[error("connection failed: {0}")]
ConnectionFailed(String),
}
impl Error {
#[must_use]
pub const fn is_network_error(&self) -> bool {
matches!(self, Self::Transport(_) | Self::Tls(_))
}
#[must_use]
pub const fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout)
}
#[must_use]
pub fn is_client_error(&self) -> bool {
matches!(self, Self::Http { status, .. } if status.is_client_error())
}
#[must_use]
pub fn is_server_error(&self) -> bool {
matches!(self, Self::Http { status, .. } if status.is_server_error())
}
#[must_use]
pub const fn is_redirect_error(&self) -> bool {
matches!(
self,
Self::TooManyRedirects { .. } | Self::InvalidRedirectLocation
)
}
#[must_use]
pub const fn is_request_error(&self) -> bool {
matches!(self, Self::InvalidRequest(_) | Self::InvalidUri(_))
}
#[must_use]
pub fn response_body(&self) -> Option<&str> {
match self {
Self::Http { response, .. } => response.body_text.as_deref(),
_ => None,
}
}
#[must_use]
pub const fn response(&self) -> Option<&Response> {
match self {
Self::Http { response, .. } => Some(&response.response),
_ => None,
}
}
#[must_use]
pub fn deserialize_http_error<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
match self {
Self::Http { response, .. } => response
.body_text
.as_ref()
.and_then(|text| serde_json::from_str(text).ok()),
_ => None,
}
}
#[must_use]
pub const fn kind(&self) -> ErrorKind {
match self {
Self::Http { .. } => ErrorKind::Http,
Self::Transport(_) => ErrorKind::Transport,
Self::Tls(_) => ErrorKind::Tls,
Self::Timeout => ErrorKind::Timeout,
Self::TooManyRedirects { .. } | Self::InvalidRedirectLocation => ErrorKind::Redirect,
Self::InvalidUri(_) | Self::InvalidRequest(_) => ErrorKind::Request,
Self::BodyParse(_) => ErrorKind::BodyParse,
Self::Cookie(_) => ErrorKind::Cookie,
Self::OAuth2(_) => ErrorKind::OAuth2,
Self::Download(_) => ErrorKind::Download,
Self::WebSocket(_) => ErrorKind::WebSocket,
Self::Io(_) => ErrorKind::Io,
Self::Other(_) => ErrorKind::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorKind {
Http,
Transport,
Tls,
Timeout,
Redirect,
Request,
BodyParse,
Cookie,
OAuth2,
Download,
WebSocket,
Io,
Other,
}
impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http => write!(f, "http"),
Self::Transport => write!(f, "transport"),
Self::Tls => write!(f, "tls"),
Self::Timeout => write!(f, "timeout"),
Self::Redirect => write!(f, "redirect"),
Self::Request => write!(f, "request"),
Self::BodyParse => write!(f, "body_parse"),
Self::Cookie => write!(f, "cookie"),
Self::OAuth2 => write!(f, "oauth2"),
Self::Download => write!(f, "download"),
Self::WebSocket => write!(f, "websocket"),
Self::Io => write!(f, "io"),
Self::Other => write!(f, "other"),
}
}
}
impl http_kit::HttpError for Error {
fn status(&self) -> StatusCode {
match self {
Self::Timeout => StatusCode::GATEWAY_TIMEOUT,
Self::Http { status, .. }
| Self::OAuth2(OAuth2ErrorKind::TokenEndpointError { status, .. })
| Self::Download(DownloadErrorKind::UpstreamError(status)) => *status,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl<E, M> From<http_kit::middleware::MiddlewareError<E, M>> for Error
where
E: Into<Self>,
M: Into<Self>,
{
fn from(err: http_kit::middleware::MiddlewareError<E, M>) -> Self {
match err {
http_kit::middleware::MiddlewareError::Endpoint(e) => e.into(),
http_kit::middleware::MiddlewareError::Middleware(m) => m.into(),
}
}
}