Skip to main content

just_common/
error.rs

1//! Shared error types for the transport layer and provider clients.
2
3use std::string::FromUtf8Error;
4use thiserror::Error;
5
6use reqwest::StatusCode;
7
8/// Errors produced by the shared HTTP/SSE transport layer.
9#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum TransportError {
12    #[error("invalid configuration: {0}")]
13    InvalidConfig(&'static str),
14
15    #[error("failed to build http client: {0}")]
16    BuildClient(#[source] reqwest::Error),
17
18    #[error("request failed: {0}")]
19    Transport(#[source] reqwest::Error),
20
21    /// A non-success HTTP status, with the full response body captured as diagnostic text.
22    ///
23    /// `body` is read under the shared size cap (`MAX_BODY_BYTES`). Bodies that fit are captured
24    /// in full; an oversized error body instead surfaces as [`BodyTooLarge`](Self::BodyTooLarge)
25    /// and this variant is not produced. So when this variant is present, `body` is complete —
26    /// never a truncated prefix.
27    #[error("api returned {status}")]
28    HttpStatus { status: StatusCode, body: String },
29
30    /// A streamed response chunk could not be deserialized.
31    ///
32    /// Produced only by the SSE event parser, one event at a time. When a
33    /// `TransportError` is lifted into a [`ProviderError`] via `From`,
34    /// this surfaces as `ProviderError::Transport(TransportError::Deserialize)` —
35    /// **not** as `ProviderError::Deserialize`, which is reserved for full-body
36    /// failures produced by `parse_json`. Consumers matching for deserialization
37    /// failures across both paths must account for both variants.
38    #[error("failed to deserialize response body: {source}")]
39    Deserialize {
40        #[source]
41        source: serde_json::Error,
42        body: String,
43    },
44
45    #[error("failed to decode streamed response as UTF-8: {0}")]
46    Utf8(#[source] FromUtf8Error),
47
48    /// A non-streaming response body exceeded the shared size cap and was not fully buffered.
49    ///
50    /// Produced by the capped body reader (`read_body_text`). Distinct from
51    /// [`InvalidResponse`](Self::InvalidResponse), which reports *content* problems (empty body,
52    /// malformed structure) rather than a size limit. The reader stops before the offending chunk
53    /// is appended, so no body text is carried here. When the overflow occurs while reading an
54    /// *error* body, the HTTP status is not carried here either — it is visible only to callers
55    /// that read status off the raw response before consuming the body (the `prepare`/`send` path).
56    #[error("response body exceeded {limit}-byte limit")]
57    BodyTooLarge { limit: usize },
58
59    #[error("invalid response: {0}")]
60    InvalidResponse(String),
61}
62
63/// Generic error type for OpenAI-compatible API provider clients.
64#[derive(Debug, Error)]
65#[non_exhaustive]
66pub enum ProviderError {
67    /// Transport-layer error from the shared HTTP/SSE layer.
68    ///
69    /// This is also where streaming/chunk deserialization failures land: an SSE
70    /// event that fails to parse originates as `TransportError::Deserialize` and
71    /// is wrapped here, **not** as the [`Deserialize`](Self::Deserialize) variant
72    /// below, which is reserved for full response-body failures from `parse_json`.
73    #[error("transport error: {0}")]
74    Transport(#[from] TransportError),
75
76    /// The request shape was invalid for the selected client method.
77    #[error("invalid request: {0}")]
78    InvalidRequest(String),
79
80    /// Failed to serialize the request body.
81    #[error("serialization failed: {0}")]
82    Serialize(#[from] serde_json::Error),
83
84    /// Failed to deserialize a full response body.
85    ///
86    /// Produced only by `parse_json`, which deserializes the entire HTTP response
87    /// body. Streaming/chunk deserialization failures are a separate concern: they
88    /// originate as `TransportError::Deserialize` and surface on
89    /// [`Transport`](Self::Transport) (via `From<TransportError>`), not here.
90    #[error("failed to deserialize response body: {source}")]
91    Deserialize {
92        #[source]
93        source: serde_json::Error,
94        body: String,
95    },
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::error::Error as StdError;
102
103    /// After dropping `#[error(transparent)]` from `ProviderError::Transport`, the wrapped
104    /// `TransportError` must be reachable by walking `source()` and downcasting — so a consumer
105    /// can recover, e.g. a 429 status from the error object. This contract would have failed
106    /// before the change: `transparent` flattened `TransportError` out of the source chain.
107    #[test]
108    fn transport_error_reachable_via_source_chain() {
109        let te = TransportError::HttpStatus {
110            status: StatusCode::TOO_MANY_REQUESTS,
111            body: "rate limited".into(),
112        };
113        let pe: ProviderError = te.into(); // ProviderError::Transport(te)
114
115        let mut cur: Option<&(dyn StdError + 'static)> = Some(&pe);
116        let mut found = None;
117        while let Some(e) = cur {
118            if let Some(t) = e.downcast_ref::<TransportError>() {
119                found = Some(t);
120                break;
121            }
122            cur = e.source();
123        }
124
125        let found = found.expect("TransportError must be reachable via the source chain");
126        assert!(
127            matches!(
128                found,
129                TransportError::HttpStatus { status, .. } if *status == StatusCode::TOO_MANY_REQUESTS
130            ),
131            "expected HttpStatus 429, got {found:?}"
132        );
133    }
134}