1use reqwest::StatusCode;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
7pub enum AnthropicError {
8 #[error("HTTP error: {0}")]
10 Reqwest(#[from] reqwest::Error),
11
12 #[error("API error: {0:?}")]
14 Api(ApiErrorObject),
15
16 #[error("Invalid configuration: {0}")]
18 Config(String),
19
20 #[error("Serialization error: {0}")]
22 Serde(String),
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ApiErrorObject {
28 pub r#type: Option<String>,
30 pub message: String,
32 pub request_id: Option<String>,
34 pub code: Option<String>,
36 #[serde(skip)]
38 pub status_code: Option<u16>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42struct ErrorEnvelope {
43 error: ApiErrorObject,
44}
45
46impl AnthropicError {
47 #[must_use]
52 pub fn is_retryable(&self) -> bool {
53 match self {
54 Self::Api(obj) => obj
55 .status_code
56 .is_some_and(crate::retry::is_retryable_status),
57 Self::Reqwest(e) => e.is_timeout() || e.is_connect(),
58 Self::Config(_) | Self::Serde(_) => false,
59 }
60 }
61}
62
63#[must_use]
67pub fn map_deser(e: &serde_json::Error, body: &[u8]) -> AnthropicError {
68 let snippet = String::from_utf8_lossy(&body[..body.len().min(400)]).to_string();
69 AnthropicError::Serde(format!("{e}: {snippet}"))
70}
71
72#[must_use]
76pub fn deserialize_api_error(status: StatusCode, body: &[u8]) -> AnthropicError {
77 let status_code = Some(status.as_u16());
78
79 if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
80 let mut error = envelope.error;
81 error.status_code = status_code;
82 return AnthropicError::Api(error);
83 }
84
85 AnthropicError::Api(ApiErrorObject {
87 r#type: Some(format!("http_{}", status.as_u16())),
88 message: String::from_utf8_lossy(body).into_owned(),
89 request_id: None,
90 code: None,
91 status_code,
92 })
93}