1use crate::xrpc::EncodeError;
4use bytes::Bytes;
5use smol_str::SmolStr;
6
7pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
9
10#[derive(Debug, thiserror::Error, miette::Diagnostic)]
12#[error("{kind}")]
13pub struct ClientError {
14 #[diagnostic_source]
15 kind: ClientErrorKind,
16 #[source]
17 source: Option<BoxError>,
18 #[help]
19 help: Option<SmolStr>,
20 context: Option<SmolStr>,
21 url: Option<SmolStr>,
22 details: Option<SmolStr>,
23 location: Option<SmolStr>,
24}
25
26#[derive(Debug, thiserror::Error, miette::Diagnostic)]
28pub enum ClientErrorKind {
29 #[error("transport error")]
31 #[diagnostic(code(jacquard::client::transport))]
32 Transport,
33
34 #[error("invalid request: {0}")]
36 #[diagnostic(
37 code(jacquard::client::invalid_request),
38 help("check request parameters and format")
39 )]
40 InvalidRequest(SmolStr),
41
42 #[error("encode error: {0}")]
44 #[diagnostic(
45 code(jacquard::client::encode),
46 help("check request body format and encoding")
47 )]
48 Encode(SmolStr),
49
50 #[error("decode error: {0}")]
52 #[diagnostic(
53 code(jacquard::client::decode),
54 help("check response format and encoding")
55 )]
56 Decode(SmolStr),
57
58 #[error("HTTP {status}")]
60 #[diagnostic(code(jacquard::client::http))]
61 Http {
62 status: http::StatusCode,
64 },
65
66 #[error("auth error: {0}")]
68 #[diagnostic(code(jacquard::client::auth))]
69 Auth(AuthError),
70
71 #[error("identity resolution failed")]
73 #[diagnostic(
74 code(jacquard::client::identity_resolution),
75 help("check handle/DID is valid and network is accessible")
76 )]
77 IdentityResolution,
78
79 #[error("storage error")]
81 #[diagnostic(
82 code(jacquard::client::storage),
83 help("check storage backend is accessible and has sufficient permissions")
84 )]
85 Storage,
86}
87
88impl ClientError {
89 pub fn new(kind: ClientErrorKind, source: Option<BoxError>) -> Self {
91 Self {
92 kind,
93 source,
94 help: None,
95 context: None,
96 url: None,
97 details: None,
98 location: None,
99 }
100 }
101
102 pub fn kind(&self) -> &ClientErrorKind {
104 &self.kind
105 }
106
107 pub fn source_err(&self) -> Option<&BoxError> {
109 self.source.as_ref()
110 }
111
112 pub fn context(&self) -> Option<&str> {
114 self.context.as_ref().map(|s| s.as_str())
115 }
116
117 pub fn url(&self) -> Option<&str> {
119 self.url.as_ref().map(|s| s.as_str())
120 }
121
122 pub fn details(&self) -> Option<&str> {
124 self.details.as_ref().map(|s| s.as_str())
125 }
126
127 pub fn location(&self) -> Option<&str> {
129 self.location.as_ref().map(|s| s.as_str())
130 }
131
132 pub fn with_help(mut self, help: impl Into<SmolStr>) -> Self {
134 self.help = Some(help.into());
135 self
136 }
137
138 pub fn with_context(mut self, context: impl Into<SmolStr>) -> Self {
140 self.context = Some(context.into());
141 self
142 }
143
144 pub fn with_url(mut self, url: impl Into<SmolStr>) -> Self {
146 self.url = Some(url.into());
147 self
148 }
149
150 pub fn with_details(mut self, details: impl Into<SmolStr>) -> Self {
152 self.details = Some(details.into());
153 self
154 }
155
156 pub fn with_location(mut self, location: impl Into<SmolStr>) -> Self {
158 self.location = Some(location.into());
159 self
160 }
161
162 pub fn transport(source: impl std::error::Error + Send + Sync + 'static) -> Self {
166 Self::new(ClientErrorKind::Transport, Some(Box::new(source)))
167 }
168
169 pub fn invalid_request(msg: impl Into<SmolStr>) -> Self {
171 Self::new(ClientErrorKind::InvalidRequest(msg.into()), None)
172 }
173
174 pub fn encode(msg: impl Into<SmolStr>) -> Self {
176 Self::new(ClientErrorKind::Encode(msg.into()), None)
177 }
178
179 pub fn decode(msg: impl Into<SmolStr>) -> Self {
181 Self::new(ClientErrorKind::Decode(msg.into()), None)
182 }
183
184 pub fn http(status: http::StatusCode, body: Option<Bytes>) -> Self {
186 let http_err = HttpError { status, body };
187 Self::new(ClientErrorKind::Http { status }, Some(Box::new(http_err)))
188 }
189
190 pub fn auth(auth_error: AuthError) -> Self {
192 Self::new(ClientErrorKind::Auth(auth_error), None)
193 }
194
195 pub fn identity_resolution(source: impl std::error::Error + Send + Sync + 'static) -> Self {
197 Self::new(ClientErrorKind::IdentityResolution, Some(Box::new(source)))
198 }
199
200 pub fn storage(source: impl std::error::Error + Send + Sync + 'static) -> Self {
202 Self::new(ClientErrorKind::Storage, Some(Box::new(source)))
203 }
204}
205
206pub type XrpcResult<T> = std::result::Result<T, ClientError>;
208
209#[derive(Debug, thiserror::Error, miette::Diagnostic)]
239pub enum DecodeError {
240 #[error("Failed to deserialize JSON: {0}")]
242 Json(
243 #[from]
244 #[source]
245 serde_json::Error,
246 ),
247 #[error("Failed to deserialize CBOR: {0}")]
249 CborLocal(
250 #[from]
251 #[source]
252 serde_ipld_dagcbor::DecodeError<std::io::Error>,
253 ),
254 #[error("Failed to deserialize CBOR: {0}")]
256 CborRemote(
257 #[from]
258 #[source]
259 serde_ipld_dagcbor::DecodeError<HttpError>,
260 ),
261 #[error("Failed to deserialize DAG-CBOR: {0}")]
263 DagCborInfallible(
264 #[from]
265 #[source]
266 serde_ipld_dagcbor::DecodeError<std::convert::Infallible>,
267 ),
268 #[cfg(feature = "websocket")]
270 #[error("Failed to deserialize cbor header: {0}")]
271 CborHeader(
272 #[from]
273 #[source]
274 ciborium::de::Error<std::io::Error>,
275 ),
276
277 #[cfg(feature = "websocket")]
279 #[error("Unknown event type: {0}")]
280 UnknownEventType(smol_str::SmolStr),
281}
282
283#[derive(Debug, thiserror::Error, miette::Diagnostic)]
285pub struct HttpError {
286 pub status: http::StatusCode,
288 pub body: Option<Bytes>,
290}
291
292impl std::fmt::Display for HttpError {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 write!(f, "HTTP {}", self.status)?;
295 if let Some(body) = &self.body {
296 if let Ok(s) = std::str::from_utf8(body) {
297 write!(f, ":\n{}", s)?;
298 }
299 }
300 Ok(())
301 }
302}
303
304#[derive(Debug, thiserror::Error, miette::Diagnostic)]
306pub enum AuthError {
307 #[error("Access token expired")]
309 TokenExpired,
310
311 #[error("Invalid access token")]
313 InvalidToken,
314
315 #[error("Token refresh failed")]
317 RefreshFailed,
318
319 #[error("No authentication provided, but endpoint requires auth")]
321 NotAuthenticated,
322
323 #[error("Authentication error: {0:?}")]
325 Other(http::HeaderValue),
326}
327
328impl crate::IntoStatic for AuthError {
329 type Output = AuthError;
330
331 fn into_static(self) -> Self::Output {
332 match self {
333 AuthError::TokenExpired => AuthError::TokenExpired,
334 AuthError::InvalidToken => AuthError::InvalidToken,
335 AuthError::RefreshFailed => AuthError::RefreshFailed,
336 AuthError::NotAuthenticated => AuthError::NotAuthenticated,
337 AuthError::Other(header) => AuthError::Other(header),
338 }
339 }
340}
341
342#[allow(deprecated)]
347impl From<DecodeError> for ClientError {
354 fn from(e: DecodeError) -> Self {
355 let msg = smol_str::format_smolstr!("{:?}", e);
356 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
357 .with_context("response deserialization failed")
358 }
359}
360
361impl From<HttpError> for ClientError {
362 fn from(e: HttpError) -> Self {
363 Self::http(e.status, e.body)
364 }
365}
366
367impl From<AuthError> for ClientError {
368 fn from(e: AuthError) -> Self {
369 Self::auth(e)
370 }
371}
372
373impl From<EncodeError> for ClientError {
374 fn from(e: EncodeError) -> Self {
375 let msg = smol_str::format_smolstr!("{:?}", e);
376 Self::new(ClientErrorKind::Encode(msg), Some(Box::new(e)))
377 .with_context("request encoding failed")
378 }
379}
380
381#[cfg(feature = "reqwest-client")]
383impl From<reqwest::Error> for ClientError {
384 #[cfg(not(target_arch = "wasm32"))]
385 fn from(e: reqwest::Error) -> Self {
386 Self::transport(e)
387 }
388
389 #[cfg(target_arch = "wasm32")]
390 fn from(e: reqwest::Error) -> Self {
391 Self::transport(e)
392 }
393}
394
395impl From<serde_json::Error> for ClientError {
397 fn from(e: serde_json::Error) -> Self {
398 let msg = smol_str::format_smolstr!("{:?}", e);
399 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
400 .with_context("JSON deserialization failed")
401 }
402}
403
404impl From<serde_ipld_dagcbor::DecodeError<std::io::Error>> for ClientError {
405 fn from(e: serde_ipld_dagcbor::DecodeError<std::io::Error>) -> Self {
406 let msg = smol_str::format_smolstr!("{:?}", e);
407 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
408 .with_context("DAG-CBOR deserialization failed (local I/O)")
409 }
410}
411
412impl From<serde_ipld_dagcbor::DecodeError<HttpError>> for ClientError {
413 fn from(e: serde_ipld_dagcbor::DecodeError<HttpError>) -> Self {
414 let msg = smol_str::format_smolstr!("{:?}", e);
415 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
416 .with_context("DAG-CBOR deserialization failed (remote)")
417 }
418}
419
420impl From<serde_ipld_dagcbor::DecodeError<std::convert::Infallible>> for ClientError {
421 fn from(e: serde_ipld_dagcbor::DecodeError<std::convert::Infallible>) -> Self {
422 let msg = smol_str::format_smolstr!("{:?}", e);
423 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
424 .with_context("DAG-CBOR deserialization failed (in-memory)")
425 }
426}
427
428#[cfg(feature = "websocket")]
429impl From<ciborium::de::Error<std::io::Error>> for ClientError {
430 fn from(e: ciborium::de::Error<std::io::Error>) -> Self {
431 let msg = smol_str::format_smolstr!("{:?}", e);
432 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
433 .with_context("CBOR header deserialization failed")
434 }
435}
436
437impl From<crate::session::SessionStoreError> for ClientError {
439 fn from(e: crate::session::SessionStoreError) -> Self {
440 Self::storage(e)
441 }
442}
443
444impl From<url::ParseError> for ClientError {
446 fn from(e: url::ParseError) -> Self {
447 Self::invalid_request(e.to_string())
448 }
449}