use std::error::Error as StdError;
use std::fmt;
use std::time::Duration;
use http::{Method, StatusCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
InvalidConfig,
Transport,
Timeout,
Decode,
Api,
Auth,
Canceled,
}
#[derive(Debug, Clone)]
pub struct RequestContext {
method: Method,
url: String,
}
impl RequestContext {
pub(crate) fn new(method: Method, url: impl Into<String>) -> Self {
Self {
method,
url: url.into(),
}
}
pub fn method(&self) -> &Method {
&self.method
}
pub fn url(&self) -> &str {
&self.url
}
}
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
inner: Box<ErrorInner>,
}
#[derive(Debug)]
struct ErrorInner {
message: String,
context: Option<RequestContext>,
status: Option<StatusCode>,
request_id: Option<String>,
retry_after: Option<Duration>,
body_snippet: Option<String>,
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
}
impl Error {
pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
Self {
kind: ErrorKind::InvalidConfig,
inner: Box::new(ErrorInner {
message: message.into(),
context: None,
status: None,
request_id: None,
retry_after: None,
body_snippet: None,
source: None,
}),
}
}
pub(crate) fn auth(message: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Auth,
inner: Box::new(ErrorInner {
message: message.into(),
context: None,
status: None,
request_id: None,
retry_after: None,
body_snippet: None,
source: None,
}),
}
}
pub(crate) fn auth_http(
context: RequestContext,
status: StatusCode,
message: impl Into<String>,
request_id: Option<String>,
body_snippet: Option<String>,
) -> Self {
Self {
kind: ErrorKind::Auth,
inner: Box::new(ErrorInner {
message: message.into(),
context: Some(context),
status: Some(status),
request_id,
retry_after: None,
body_snippet,
source: None,
}),
}
}
pub(crate) fn transport(
context: RequestContext,
message: impl Into<String>,
source: impl StdError + Send + Sync + 'static,
) -> Self {
Self {
kind: ErrorKind::Transport,
inner: Box::new(ErrorInner {
message: message.into(),
context: Some(context),
status: None,
request_id: None,
retry_after: None,
body_snippet: None,
source: Some(Box::new(source)),
}),
}
}
pub(crate) fn timeout(
context: RequestContext,
message: impl Into<String>,
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
) -> Self {
Self {
kind: ErrorKind::Timeout,
inner: Box::new(ErrorInner {
message: message.into(),
context: Some(context),
status: None,
request_id: None,
retry_after: None,
body_snippet: None,
source,
}),
}
}
pub(crate) fn decode(
context: RequestContext,
message: impl Into<String>,
body_snippet: Option<String>,
source: impl StdError + Send + Sync + 'static,
) -> Self {
Self {
kind: ErrorKind::Decode,
inner: Box::new(ErrorInner {
message: message.into(),
context: Some(context),
status: None,
request_id: None,
retry_after: None,
body_snippet,
source: Some(Box::new(source)),
}),
}
}
pub(crate) fn api(
context: RequestContext,
status: StatusCode,
message: impl Into<String>,
request_id: Option<String>,
retry_after: Option<Duration>,
body_snippet: Option<String>,
) -> Self {
Self {
kind: ErrorKind::Api,
inner: Box::new(ErrorInner {
message: message.into(),
context: Some(context),
status: Some(status),
request_id,
retry_after,
body_snippet,
source: None,
}),
}
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn status(&self) -> Option<StatusCode> {
self.inner.status
}
pub fn request_id(&self) -> Option<&str> {
self.inner.request_id.as_deref()
}
pub fn retry_after(&self) -> Option<Duration> {
self.inner.retry_after
}
pub fn body_snippet(&self) -> Option<&str> {
self.inner.body_snippet.as_deref()
}
pub fn context(&self) -> Option<&RequestContext> {
self.inner.context.as_ref()
}
pub fn is_retryable(&self) -> bool {
match self.kind {
ErrorKind::Timeout | ErrorKind::Transport => true,
ErrorKind::Api => matches!(
self.inner.status,
Some(StatusCode::TOO_MANY_REQUESTS)
| Some(StatusCode::BAD_GATEWAY)
| Some(StatusCode::SERVICE_UNAVAILABLE)
| Some(StatusCode::GATEWAY_TIMEOUT)
),
_ => false,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.inner.context, self.inner.status) {
(Some(ctx), Some(status)) => write!(
f,
"{} (status {}, {} {})",
self.inner.message,
status.as_u16(),
ctx.method.as_str(),
ctx.url
)?,
(Some(ctx), None) => write!(
f,
"{} ({} {})",
self.inner.message,
ctx.method.as_str(),
ctx.url
)?,
(None, Some(status)) => {
write!(f, "{} (status {})", self.inner.message, status.as_u16())?
}
(None, None) => write!(f, "{}", self.inner.message)?,
}
if let Some(id) = &self.inner.request_id {
write!(f, ", request-id={id}")?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.inner
.source
.as_deref()
.map(|err| err as &(dyn StdError + 'static))
}
}