use std::time::Duration;
use serde_json::Value;
use super::envelope::{extract_code, extract_message, MAX_ERROR_BODY_LENGTH};
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: u16,
pub code: Option<String>,
pub message: Option<String>,
pub body: Option<Value>,
pub request_id: Option<String>,
pub retry_after: Option<Duration>,
pub endpoint: Option<String>,
}
impl ApiError {
pub(crate) fn from_response(
status: u16,
request_id: Option<String>,
retry_after: Option<Duration>,
body: &[u8],
endpoint: String,
) -> Self {
let parsed: Option<Value> = if body.is_empty() {
None
} else {
serde_json::from_slice(body).ok()
};
let message = extract_message(parsed.as_ref()).or_else(|| {
if body.is_empty() {
return None;
}
let raw = String::from_utf8_lossy(body);
let mut text = raw.chars().take(MAX_ERROR_BODY_LENGTH).collect::<String>();
if raw.chars().count() > MAX_ERROR_BODY_LENGTH {
text.push('…');
}
Some(text)
});
Self {
status,
code: extract_code(parsed.as_ref()),
message,
body: parsed,
request_id,
retry_after,
endpoint: Some(endpoint),
}
}
pub fn kind(&self) -> ApiErrorKind {
ApiErrorKind::from_status(self.status)
}
pub fn is_rate_limit(&self) -> bool {
self.status == 429
}
pub fn is_overloaded(&self) -> bool {
self.status == 529
}
pub fn is_retryable_later(&self) -> bool {
self.is_rate_limit() || self.is_overloaded()
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.status)?;
if let Some(code) = &self.code {
write!(f, " {code}")?;
}
if let Some(message) = &self.message {
write!(f, ": ")?;
for ch in message.chars().take(super::envelope::MAX_ERROR_BODY_LENGTH) {
write!(f, "{ch}")?;
}
if message
.chars()
.nth(super::envelope::MAX_ERROR_BODY_LENGTH)
.is_some()
{
write!(f, "…")?;
}
}
if let Some(endpoint) = &self.endpoint {
write!(f, " ({endpoint})")?;
}
if let Some(request_id) = &self.request_id {
write!(f, " (request_id={request_id})")?;
}
Ok(())
}
}
impl std::error::Error for ApiError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApiErrorKind {
BadRequest,
Authentication,
PermissionDenied,
NotFound,
PayloadTooLarge,
UnprocessableEntity,
RateLimit,
Overloaded,
InternalServer,
Other,
}
impl ApiErrorKind {
pub fn from_status(status: u16) -> Self {
match status {
400 => Self::BadRequest,
401 => Self::Authentication,
403 => Self::PermissionDenied,
404 => Self::NotFound,
413 => Self::PayloadTooLarge,
422 => Self::UnprocessableEntity,
429 => Self::RateLimit,
529 => Self::Overloaded,
s if (500..600).contains(&s) => Self::InternalServer,
_ => Self::Other,
}
}
}