use reqwest::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AnthropicError {
#[error("HTTP error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("API error: {0:?}")]
Api(ApiErrorObject),
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Serialization error: {0}")]
Serde(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiErrorObject {
pub r#type: Option<String>,
pub message: String,
pub request_id: Option<String>,
pub code: Option<String>,
#[serde(skip)]
pub status_code: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ErrorEnvelope {
error: ApiErrorObject,
}
impl AnthropicError {
#[must_use]
pub fn is_retryable(&self) -> bool {
match self {
Self::Api(obj) => obj
.status_code
.is_some_and(crate::retry::is_retryable_status),
Self::Reqwest(e) => e.is_timeout() || e.is_connect(),
Self::Config(_) | Self::Serde(_) => false,
}
}
}
#[must_use]
pub fn map_deser(e: &serde_json::Error, body: &[u8]) -> AnthropicError {
let snippet = String::from_utf8_lossy(&body[..body.len().min(400)]).to_string();
AnthropicError::Serde(format!("{e}: {snippet}"))
}
#[must_use]
pub fn deserialize_api_error(status: StatusCode, body: &[u8]) -> AnthropicError {
let status_code = Some(status.as_u16());
if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
let mut error = envelope.error;
error.status_code = status_code;
return AnthropicError::Api(error);
}
AnthropicError::Api(ApiErrorObject {
r#type: Some(format!("http_{}", status.as_u16())),
message: String::from_utf8_lossy(body).into_owned(),
request_id: None,
code: None,
status_code,
})
}