Skip to main content

salvor_llm/
error.rs

1//! Error types for the Messages API client.
2//!
3//! Every failure the client can produce is one variant of [`Error`]. The
4//! variants are grouped so that the retry logic in [`crate::Client`] can ask a
5//! single question about any failure: "is this worth trying again?" See
6//! [`Error::is_retryable`].
7
8use std::time::Duration;
9
10use thiserror::Error;
11
12/// A structured error returned by the Messages API for a non-2xx response.
13///
14/// The wire body of an API error looks like
15/// `{"type":"error","error":{"type":...,"message":...},"request_id":...}`.
16/// This struct carries the parts a caller acts on: the HTTP status, the API's
17/// own error type string, the human-readable message, and (for `429`) the
18/// `retry-after` delay the server asked us to wait.
19#[derive(Debug, Clone)]
20pub struct ApiError {
21    /// The HTTP status code of the response (for example `400` or `429`).
22    pub status: u16,
23    /// The API's error type string, taken from `error.type` in the body
24    /// (for example `invalid_request_error` or `rate_limit_error`).
25    pub kind: String,
26    /// The human-readable message from `error.message`.
27    pub message: String,
28    /// The `request-id` response header, when the server sent one. Useful when
29    /// reporting a failure to Anthropic.
30    pub request_id: Option<String>,
31    /// The `retry-after` delay parsed from the response header, when present.
32    /// Only a `429` response is expected to set this.
33    pub retry_after: Option<Duration>,
34}
35
36/// Everything that can go wrong when calling the Messages API.
37#[derive(Debug, Error)]
38pub enum Error {
39    /// The HTTP request never produced a usable response: a connection failure,
40    /// a timeout, or a request that could not be built or sent. Treated as
41    /// retryable, since these are usually transient.
42    #[error("HTTP transport failure while calling the Messages API")]
43    Transport(#[source] reqwest::Error),
44
45    /// A 2xx response arrived but its body did not deserialize into the
46    /// expected shape. Not retried: the same body would fail again.
47    #[error("could not deserialize the Messages API response body")]
48    Decode(#[source] serde_json::Error),
49
50    /// The API returned a non-2xx response with a well-formed error envelope.
51    /// Retryable only for `429`, `500`, and `529`.
52    #[error("Messages API returned HTTP {} ({}): {}", .0.status, .0.kind, .0.message)]
53    Api(ApiError),
54
55    /// A non-2xx response whose body was not a recognizable error envelope. The
56    /// raw body is preserved so the caller can see what the server actually
57    /// sent. Not retried.
58    #[error("unexpected HTTP {status} response from the Messages API: {body}")]
59    Unexpected {
60        /// The HTTP status code of the response.
61        status: u16,
62        /// The raw response body, decoded lossily as UTF-8.
63        body: String,
64    },
65}
66
67impl Error {
68    /// Whether trying the request again could plausibly succeed.
69    ///
70    /// Transport failures are always worth a retry. API errors are retried for
71    /// the three statuses the protocol calls out as transient: `429` (rate
72    /// limited), `500` (server error), and `529` (overloaded). Decode failures
73    /// and other non-2xx responses are permanent for this request.
74    #[must_use]
75    pub fn is_retryable(&self) -> bool {
76        match self {
77            Error::Transport(_) => true,
78            Error::Api(api) => matches!(api.status, 429 | 500 | 529),
79            // A 429/500/529 whose body was not a recognizable envelope is still
80            // a transient status and worth retrying.
81            Error::Unexpected { status, .. } => matches!(status, 429 | 500 | 529),
82            Error::Decode(_) => false,
83        }
84    }
85
86    /// The server-requested wait before retrying, when the response carried a
87    /// `retry-after` header. The retry loop honours this in preference to its
88    /// own backoff schedule.
89    #[must_use]
90    pub fn retry_after(&self) -> Option<Duration> {
91        match self {
92            Error::Api(api) => api.retry_after,
93            _ => None,
94        }
95    }
96}