1//! Error types for XRPC client operations
2
3use crate::xrpc::EncodeError;
4use bytes::Bytes;
5
6/// Client error type wrapping all possible error conditions
7#[derive(Debug, thiserror::Error, miette::Diagnostic)]
8pub enum ClientError {
9 /// HTTP transport error
10 #[error("HTTP transport error: {0}")]
11 Transport(
12 #[from]
13 #[diagnostic_source]
14 TransportError,
15 ),
16
17 /// Request serialization failed
18 #[error("{0}")]
19 Encode(
20 #[from]
21 #[diagnostic_source]
22 EncodeError,
23 ),
24
25 /// Response deserialization failed
26 #[error("{0}")]
27 Decode(
28 #[from]
29 #[diagnostic_source]
30 DecodeError,
31 ),
32
33 /// HTTP error response
34 #[error("HTTP {0}")]
35 Http(
36 #[from]
37 #[diagnostic_source]
38 HttpError,
39 ),
40
41 /// Authentication error
42 #[error("Authentication error: {0}")]
43 Auth(
44 #[from]
45 #[diagnostic_source]
46 AuthError,
47 ),
48}
49
50/// Transport-level errors that occur during HTTP communication
51#[derive(Debug, thiserror::Error, miette::Diagnostic)]
52pub enum TransportError {
53 /// Failed to establish connection to server
54 #[error("Connection error: {0}")]
55 Connect(String),
56
57 /// Request timed out
58 #[error("Request timeout")]
59 Timeout,
60
61 /// Request construction failed (malformed URI, headers, etc.)
62 #[error("Invalid request: {0}")]
63 InvalidRequest(String),
64
65 /// Other transport error
66 #[error("Transport error: {0}")]
67 Other(Box<dyn std::error::Error + Send + Sync>),
68}
69
70/// Response deserialization errors
71#[derive(Debug, thiserror::Error, miette::Diagnostic)]
72pub enum DecodeError {
73 /// JSON deserialization failed
74 #[error("Failed to deserialize JSON: {0}")]
75 Json(
76 #[from]
77 #[source]
78 serde_json::Error,
79 ),
80 /// CBOR deserialization failed (local I/O)
81 #[error("Failed to deserialize CBOR: {0}")]
82 CborLocal(
83 #[from]
84 #[source]
85 serde_ipld_dagcbor::DecodeError<std::io::Error>,
86 ),
87 /// CBOR deserialization failed (remote/reqwest)
88 #[error("Failed to deserialize CBOR: {0}")]
89 CborRemote(
90 #[from]
91 #[source]
92 serde_ipld_dagcbor::DecodeError<HttpError>,
93 ),
94}
95
96/// HTTP error response (non-200 status codes outside of XRPC error handling)
97#[derive(Debug, thiserror::Error, miette::Diagnostic)]
98pub struct HttpError {
99 /// HTTP status code
100 pub status: http::StatusCode,
101 /// Response body if available
102 pub body: Option<Bytes>,
103}
104
105impl std::fmt::Display for HttpError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(f, "HTTP {}", self.status)?;
108 if let Some(body) = &self.body {
109 if let Ok(s) = std::str::from_utf8(body) {
110 write!(f, ":\n{}", s)?;
111 }
112 }
113 Ok(())
114 }
115}
116
117/// Result type for client operations
118pub type XrpcResult<T> = std::result::Result<T, ClientError>;
119
120#[cfg(feature = "reqwest-client")]
121impl From<reqwest::Error> for TransportError {
122 fn from(e: reqwest::Error) -> Self {
123 if e.is_timeout() {
124 Self::Timeout
125 } else if e.is_connect() {
126 Self::Connect(e.to_string())
127 } else if e.is_builder() || e.is_request() {
128 Self::InvalidRequest(e.to_string())
129 } else {
130 Self::Other(Box::new(e))
131 }
132 }
133}
134
135/// Authentication and authorization errors
136#[derive(Debug, thiserror::Error, miette::Diagnostic)]
137pub enum AuthError {
138 /// Access token has expired (use refresh token to get a new one)
139 #[error("Access token expired")]
140 TokenExpired,
141
142 /// Access token is invalid or malformed
143 #[error("Invalid access token")]
144 InvalidToken,
145
146 /// Token refresh request failed
147 #[error("Token refresh failed")]
148 RefreshFailed,
149
150 /// Request requires authentication but none was provided
151 #[error("No authentication provided, but endpoint requires auth")]
152 NotAuthenticated,
153
154 /// Other authentication error
155 #[error("Authentication error: {0:?}")]
156 Other(http::HeaderValue),
157}
158
159impl crate::IntoStatic for AuthError {
160 type Output = AuthError;
161
162 fn into_static(self) -> Self::Output {
163 match self {
164 AuthError::TokenExpired => AuthError::TokenExpired,
165 AuthError::InvalidToken => AuthError::InvalidToken,
166 AuthError::RefreshFailed => AuthError::RefreshFailed,
167 AuthError::NotAuthenticated => AuthError::NotAuthenticated,
168 AuthError::Other(header) => AuthError::Other(header),
169 }
170 }
171}