Skip to main content

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}