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}