use std::fmt;
#[derive(Debug, Clone)]
pub struct ErrorDetails {
pub message: String,
pub status: Option<u16>,
pub code: Option<String>,
pub request_id: Option<String>,
pub feature: Option<String>,
pub retry_after_seconds: Option<u64>,
pub body: Option<serde_json::Value>,
}
impl fmt::Display for ErrorDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
if let Some(status) = self.status {
write!(f, " (status={status}")?;
if let Some(rid) = &self.request_id {
write!(f, " request_id={rid}")?;
}
write!(f, ")")?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("authentication failed: {0}")]
Auth(Box<ErrorDetails>),
#[error("plan does not allow this feature: {0}")]
Plan(Box<ErrorDetails>),
#[error("rate limit exceeded: {0}")]
RateLimit(Box<ErrorDetails>),
#[error("invalid request: {0}")]
Validation(Box<ErrorDetails>),
#[error("not found: {0}")]
NotFound(Box<ErrorDetails>),
#[error("conflict: {0}")]
Conflict(Box<ErrorDetails>),
#[error("gateway error: {0}")]
Server(Box<ErrorDetails>),
#[error("api error: {0}")]
Api(Box<ErrorDetails>),
#[error("request timed out: {0}")]
Timeout(String),
#[error("connection error: {0}")]
Connection(#[source] reqwest::Error),
#[error("failed to decode response: {0}")]
Decode(String),
#[error("invalid configuration: {0}")]
Config(String),
}
impl Error {
#[must_use]
pub fn details(&self) -> Option<&ErrorDetails> {
match self {
Error::Auth(d)
| Error::Plan(d)
| Error::RateLimit(d)
| Error::Validation(d)
| Error::NotFound(d)
| Error::Conflict(d)
| Error::Server(d)
| Error::Api(d) => Some(d),
Error::Timeout(_) | Error::Connection(_) | Error::Decode(_) | Error::Config(_) => None,
}
}
#[must_use]
pub fn status(&self) -> Option<u16> {
self.details().and_then(|d| d.status)
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
self.details().and_then(|d| d.request_id.as_deref())
}
#[must_use]
pub fn feature(&self) -> Option<&str> {
self.details().and_then(|d| d.feature.as_deref())
}
#[must_use]
pub fn retry_after_seconds(&self) -> Option<u64> {
self.details().and_then(|d| d.retry_after_seconds)
}
}
pub(crate) fn from_status(
status: u16,
body: Option<serde_json::Value>,
request_id: Option<String>,
retry_after_seconds: Option<u64>,
) -> Error {
let err_obj = body.as_ref().and_then(|b| b.get("error"));
let message = err_obj
.and_then(|e| e.get("message"))
.and_then(|m| m.as_str())
.map(str::to_owned)
.unwrap_or_else(|| format!("HTTP {status}"));
let code = err_obj
.and_then(|e| e.get("code"))
.and_then(|c| c.as_str())
.map(str::to_owned);
let feature = err_obj
.and_then(|e| e.get("feature"))
.and_then(|f| f.as_str())
.map(str::to_owned);
let details = Box::new(ErrorDetails {
message,
status: Some(status),
code,
request_id,
feature: feature.clone(),
retry_after_seconds,
body,
});
match status {
400 => Error::Validation(details),
401 => Error::Auth(details),
403 if feature.is_some() => Error::Plan(details),
403 => Error::Auth(details),
404 => Error::NotFound(details),
409 => Error::Conflict(details),
429 => Error::RateLimit(details),
s if s >= 500 => Error::Server(details),
_ => Error::Api(details),
}
}
pub type Result<T> = std::result::Result<T, Error>;