anthropic/error.rs
1//! Error types.
2//!
3//! All public error reporting flows through [`Error`]. The variant set is
4//! marked `#[non_exhaustive]` so adding a new failure mode (e.g. `Stream`
5//! when streaming lands in v0.2) is not a breaking change.
6
7use serde::{Deserialize, Serialize};
8
9use crate::types::messages::ApiErrorBody;
10
11/// Crate-wide result alias.
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// Every failure mode the SDK surfaces.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum Error {
18 /// Transport-level failure from `reqwest` (DNS, TLS handshake, connection
19 /// reset, response framing). Body decoding errors that come through
20 /// `reqwest::Response::json` also land here.
21 #[error("HTTP transport error: {0}")]
22 Http(#[from] reqwest::Error),
23
24 /// The server returned a non-2xx status. The `request_id` header is
25 /// preserved so callers can quote it in support tickets.
26 #[error("API error {status}{}: {message}", error_type.as_ref().map(|t| format!(" ({t:?})")).unwrap_or_default())]
27 Api {
28 /// HTTP status returned by the server.
29 status: http::StatusCode,
30 /// Parsed `error.type` from the response body, if it matched a known
31 /// category; `None` when the body was non-JSON.
32 error_type: Option<ErrorType>,
33 /// Human-readable message lifted from `error.message`.
34 message: String,
35 /// Value of the `request-id` response header, if present.
36 request_id: Option<String>,
37 /// Raw response body — present even when JSON decoding succeeded so
38 /// callers can log the original payload.
39 raw: String,
40 },
41
42 /// JSON serialization failed (request body) or deserialization failed
43 /// (response body) with a 2xx status. 4xx/5xx bodies fall under
44 /// [`Error::Api`] regardless of whether the body parsed.
45 #[error("JSON encode/decode error: {0}")]
46 Serde(#[from] serde_json::Error),
47
48 /// Bad client configuration (e.g. missing API key, malformed base URL).
49 /// Surfaced from `Client::from_env`, `ClientBuilder::build`, etc.
50 #[error("invalid configuration: {0}")]
51 Config(String),
52}
53
54/// Categorized error types returned in the `error.type` field of the
55/// API's JSON error body. Mirrors the Go SDK's `shared/constant` enum.
56///
57/// New API error categories deserialize as [`ErrorType::Unknown`] so the
58/// SDK doesn't break when the API gains a new error variant.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61#[non_exhaustive]
62#[allow(missing_docs)] // variant names mirror the upstream API strings 1:1
63pub enum ErrorType {
64 InvalidRequestError,
65 AuthenticationError,
66 BillingError,
67 PermissionError,
68 NotFoundError,
69 RateLimitError,
70 TimeoutError,
71 ApiError,
72 OverloadedError,
73 GatewayTimeoutError,
74 /// Catch-all for new categories the API may add.
75 #[serde(other)]
76 Unknown,
77}
78
79/// Build an [`Error::Api`] from a parsed body, falling back to the raw text
80/// when the JSON shape didn't match.
81pub(crate) fn api_error_from_response(
82 status: http::StatusCode,
83 request_id: Option<String>,
84 raw: String,
85) -> Error {
86 if let Ok(body) = serde_json::from_str::<ApiErrorBody>(&raw) {
87 let error_type =
88 serde_json::from_value::<ErrorType>(serde_json::Value::String(body.error.kind.clone()))
89 .ok();
90 Error::Api {
91 status,
92 error_type,
93 message: body.error.message,
94 request_id,
95 raw,
96 }
97 } else {
98 Error::Api {
99 status,
100 error_type: None,
101 message: format!("non-JSON error body (HTTP {})", status.as_u16()),
102 request_id,
103 raw,
104 }
105 }
106}