Skip to main content

jmap_base_client/
error.rs

1#[non_exhaustive]
2#[derive(Debug, thiserror::Error)]
3pub enum ClientError {
4    /// Network or TLS error from the HTTP layer. May be retriable (transient
5    /// network failure) or permanent (TLS configuration error). Indicates a
6    /// network or transport problem, not a JMAP protocol error.
7    ///
8    /// **Semver note**: this variant embeds `reqwest::Error` directly. Callers
9    /// that match this variant are semver-locked to the same `reqwest` major
10    /// version as this crate. This is a known pre-1.0 limitation.
11    #[error("HTTP error: {0}")]
12    Http(#[from] reqwest::Error),
13
14    /// A header value could not be encoded. Indicates a caller bug — the
15    /// credential string contains characters that are not valid HTTP header
16    /// value characters. Not retriable.
17    #[error("invalid header value: {0}")]
18    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
19
20    /// The server returned HTTP 401 (authentication failure) or 403
21    /// (authorization failure — credentials present but insufficient). Not
22    /// retriable without correcting credentials.
23    #[error("authentication or authorization failure: HTTP {0}")]
24    AuthFailed(u16),
25
26    /// A server response could not be parsed or did not match the expected
27    /// shape. Indicates the server sent a malformed response. Not retriable
28    /// without a server fix.
29    ///
30    /// Construct explicitly: `.map_err(ClientError::Parse)`.
31    #[error("parse error: {0}")]
32    Parse(serde_json::Error),
33
34    /// Downloaded blob SHA-256 does not match the expected digest. Indicates
35    /// in-transit corruption or a misbehaving server. Not retriable without
36    /// re-fetching metadata.
37    #[error("blob integrity check failed: expected {expected}, got {actual}")]
38    BlobIntegrityMismatch { expected: String, actual: String },
39
40    /// A caller-supplied argument violates a precondition (e.g. empty token,
41    /// colon in BasicAuth username, missing required filter field).
42    #[error("invalid argument: {0}")]
43    InvalidArgument(String),
44
45    /// The JMAP Session object from the server was missing a required field.
46    /// Indicates a server-side bug or incompatible server. Not retriable.
47    #[error("invalid session: {0}")]
48    InvalidSession(String),
49
50    /// The JMAP API response did not contain the expected method call ID.
51    /// Indicates a server-side bug or unexpected response shape.
52    #[error("method not found in response: {0}")]
53    MethodNotFound(String),
54
55    /// The JMAP server returned a method-level error object (RFC 8620 §3.6).
56    /// Retriability depends on `error_type` (e.g. `serverFail` may be
57    /// retried; `invalidArguments` is not retriable).
58    ///
59    /// `description` is `None` when the server omits the optional description field.
60    #[error("JMAP method error: {error_type}")]
61    MethodError {
62        error_type: String,
63        description: Option<String>,
64    },
65
66    /// A JMAP request could not be serialized to JSON when sending over
67    /// WebSocket. Indicates a caller bug — the data structure contains
68    /// non-serializable values. Not retriable.
69    ///
70    /// This error is only returned by [`WsSession::send_request`]; the HTTP
71    /// `call()` path delegates serialization to reqwest, which surfaces
72    /// serialization failures as [`ClientError::Http`].
73    ///
74    /// Construct explicitly: `.map_err(ClientError::Serialize)`.
75    #[error("serialization error: {0}")]
76    Serialize(serde_json::Error),
77
78    /// An SSE frame exceeded the configured buffer limit
79    /// ([`ClientConfig::max_sse_frame`]). The stream is terminated after this
80    /// error. Indicates a misbehaving or hostile server.
81    #[error("SSE frame too large (limit: {limit} bytes)")]
82    SseFrameTooLarge { limit: usize },
83
84    /// A server response body exceeded the enforced size limit. Protects
85    /// against unbounded memory allocation from malicious or buggy servers.
86    /// `actual` is in bytes (from Content-Length or actual read size).
87    #[error("response too large: {actual} bytes exceeds limit of {limit} bytes")]
88    ResponseTooLarge { actual: u64, limit: u64 },
89
90    /// A WebSocket transport error (connection, framing, or TLS). May be
91    /// retriable (transient network failure) or permanent (TLS config error).
92    ///
93    /// **Semver note**: this variant embeds `tungstenite::Error` directly.
94    /// Callers that match this variant are semver-locked to the same
95    /// `tokio-tungstenite` major version as this crate. Pre-1.0 limitation.
96    #[error("WebSocket error: {0}")]
97    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
98
99    /// The server returned a response that violates the JMAP protocol (outside
100    /// the Session fetch path). Examples: wrong `Content-Type` on an SSE
101    /// connection, unexpected response shape on a non-session endpoint.
102    ///
103    /// Distinct from [`ClientError::InvalidSession`], which indicates a
104    /// problem with the Session document itself. Not retriable without a
105    /// server fix.
106    #[error("unexpected server response: {0}")]
107    UnexpectedResponse(String),
108}
109
110// ---------------------------------------------------------------------------
111// Tests
112// ---------------------------------------------------------------------------
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    /// Verify ClientError does not have a RateLimited variant by exhaustive match.
119    /// This match will fail to compile if RateLimited is ever reintroduced.
120    #[test]
121    fn client_error_no_rate_limited_variant() {
122        let e = ClientError::InvalidArgument("test".into());
123        match e {
124            ClientError::Http(_) => {}
125            ClientError::InvalidHeaderValue(_) => {}
126            ClientError::AuthFailed(_) => {}
127            ClientError::Parse(_) => {}
128            ClientError::BlobIntegrityMismatch { .. } => {}
129            ClientError::InvalidArgument(_) => {}
130            ClientError::InvalidSession(_) => {}
131            ClientError::MethodNotFound(_) => {}
132            ClientError::MethodError { .. } => {}
133            ClientError::Serialize(_) => {}
134            ClientError::SseFrameTooLarge { .. } => {}
135            ClientError::ResponseTooLarge { .. } => {}
136            ClientError::WebSocket(_) => {}
137            ClientError::UnexpectedResponse(_) => {}
138        }
139    }
140}