openkind_client/error/mod.rs
1//! Error taxonomy and header parsing for the client, mirroring the TypeSafe
2//! Python SDK exception hierarchy:
3//!
4//! | Rust | Python SDK | HTTP | Server `code` |
5//! |-----------------------------------|-----------------------------------|------|----------------------|
6//! | [`ApiErrorKind::BadRequest`] | `TypeSafeBadRequestError` | 400 | `bad_json` |
7//! | [`ApiErrorKind::Authentication`] | `TypeSafeAuthenticationError` | 401 | `unauthorized` |
8//! | [`ApiErrorKind::NotFound`] | `TypeSafeNotFoundError` | 404 | `unknown_model` |
9//! | [`ApiErrorKind::PayloadTooLarge`] | — | 413 | `payload_too_large` |
10//! | [`ApiErrorKind::UnprocessableEntity`] | `TypeSafeUnprocessableEntityError` | 422 | `invalid_body` |
11//! | [`ApiErrorKind::RateLimit`] | `TypeSafeRateLimitError` | 429 | `rate_limited` |
12//! | [`ApiErrorKind::Overloaded`] | `TypeSafeAPIOverloadedError` | 529 | `overloaded` |
13//! | [`ApiErrorKind::InternalServer`] | `TypeSafeInternalServerError` | 5xx | `backend_error` / `internal_error` |
14//!
15//! 429/529 responses carry `retry-after-ms` (milliseconds, preferred) and
16//! `Retry-After` (seconds, or an HTTP-date) headers; both are parsed into
17//! [`ApiError::retry_after`] so [`crate::RetryPolicy`] can back off exactly
18//! as long as the server asks.
19
20use std::time::Duration;
21
22mod api_error;
23mod envelope;
24mod retry_after;
25
26#[cfg(test)]
27mod tests;
28
29pub use api_error::{ApiError, ApiErrorKind};
30pub use retry_after::parse_retry_after;
31pub(crate) use retry_after::parse_retry_after_with;
32pub(crate) use retry_after::REQUEST_ID_HEADER;
33
34/// All client failures. [`Error::Api`] wraps the unsuccessful-response case;
35/// the other variants cover transport, timeout, decoding, response validation,
36/// and configuration problems.
37#[derive(Debug, thiserror::Error)]
38pub enum Error {
39 /// The server responded with a non-success status. See [`ApiError`].
40 ///
41 /// (Boxed to keep `Result<T, Error>` small; deref coercion makes the
42 /// inner [`ApiError`] fields directly accessible.)
43 #[error("{0}")]
44 Api(#[from] Box<ApiError>),
45
46 /// The request could not reach, or the response could not be read from,
47 /// the server.
48 #[error("connection error: {0}")]
49 Connection(#[source] Box<dyn std::error::Error + Send + Sync>),
50
51 /// The request exceeded its per-attempt timeout.
52 #[error("request timed out after {timeout:?}")]
53 Timeout {
54 /// The timeout that was in effect for the failed attempt.
55 timeout: Duration,
56 },
57
58 /// A response body exceeded the client's fixed memory-safety limit.
59 #[error("response body exceeded the {limit}-byte limit")]
60 ResponseTooLarge {
61 /// Maximum response-body size accepted by the client.
62 limit: usize,
63 },
64
65 /// A 2xx response body could not be decoded as the expected type.
66 #[error("failed to decode response body (status {status}): {source}")]
67 Decode {
68 /// HTTP status of the undecodable response.
69 status: u16,
70 /// The first 200 chars of the raw body, to aid debugging.
71 body_excerpt: String,
72 /// The underlying JSON error.
73 #[source]
74 source: serde_json::Error,
75 },
76
77 /// A 2xx response decoded but violated the submitted question contract.
78 #[error("invalid response: {source}")]
79 InvalidResponse {
80 /// The violated request/response invariant. Response content is omitted.
81 #[source]
82 source: openkind_core::ValidationError,
83 },
84
85 /// The client was constructed with invalid settings (missing API key,
86 /// bad base URL, non-positive timeout, ...).
87 #[error("invalid client configuration: {0}")]
88 Config(String),
89}
90
91impl Error {
92 /// HTTP status, when the error originated from a server response.
93 pub fn status(&self) -> Option<u16> {
94 match self {
95 Error::Api(api) => Some(api.status),
96 _ => None,
97 }
98 }
99
100 /// The server-requested retry delay, when present.
101 pub fn retry_after(&self) -> Option<Duration> {
102 match self {
103 Error::Api(api) => api.retry_after,
104 _ => None,
105 }
106 }
107
108 /// The `x-typesafe-request-id` of the failed request, when present.
109 pub fn request_id(&self) -> Option<&str> {
110 match self {
111 Error::Api(api) => api.request_id.as_deref(),
112 _ => None,
113 }
114 }
115
116 /// The error kind classification, when this is an API error.
117 pub fn kind(&self) -> Option<ApiErrorKind> {
118 match self {
119 Error::Api(api) => Some(api.kind()),
120 _ => None,
121 }
122 }
123}