Skip to main content

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}