1use crate::xrpc::EncodeError;
4use alloc::boxed::Box;
5use alloc::string::ToString;
6use bytes::Bytes;
7use smol_str::SmolStr;
8
9#[cfg(feature = "std")]
10use miette::Diagnostic;
11
12pub type BoxError = Box<dyn core::error::Error + Send + Sync + 'static>;
14
15#[derive(Debug, thiserror::Error)]
17#[cfg_attr(feature = "std", derive(Diagnostic))]
18#[error("{kind}")]
19pub struct ClientError {
20 #[cfg_attr(feature = "std", diagnostic_source)]
21 kind: ClientErrorKind,
22 #[source]
23 source: Option<BoxError>,
24 #[cfg_attr(feature = "std", help)]
25 help: Option<SmolStr>,
26 context: Option<SmolStr>,
27 url: Option<SmolStr>,
28 details: Option<SmolStr>,
29 location: Option<SmolStr>,
30}
31
32#[derive(Debug, thiserror::Error)]
34#[cfg_attr(feature = "std", derive(Diagnostic))]
35#[non_exhaustive]
36pub enum ClientErrorKind {
37 #[error("transport error")]
39 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::transport)))]
40 Transport,
41
42 #[error("invalid request: {0}")]
44 #[cfg_attr(
45 feature = "std",
46 diagnostic(
47 code(jacquard::client::invalid_request),
48 help("check request parameters and format")
49 )
50 )]
51 InvalidRequest(SmolStr),
52
53 #[error("encode error: {0}")]
55 #[cfg_attr(
56 feature = "std",
57 diagnostic(
58 code(jacquard::client::encode),
59 help("check request body format and encoding")
60 )
61 )]
62 Encode(SmolStr),
63
64 #[error("decode error: {0}")]
66 #[cfg_attr(
67 feature = "std",
68 diagnostic(
69 code(jacquard::client::decode),
70 help("check response format and encoding")
71 )
72 )]
73 Decode(SmolStr),
74
75 #[error("HTTP {status}")]
77 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::http)))]
78 Http {
79 status: http::StatusCode,
81 },
82
83 #[error("auth error: {0}")]
85 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::auth)))]
86 Auth(AuthError),
87
88 #[error("identity resolution failed")]
90 #[cfg_attr(
91 feature = "std",
92 diagnostic(
93 code(jacquard::client::identity_resolution),
94 help("check handle/DID is valid and network is accessible")
95 )
96 )]
97 IdentityResolution,
98
99 #[error("storage error")]
101 #[cfg_attr(
102 feature = "std",
103 diagnostic(
104 code(jacquard::client::storage),
105 help("check storage backend is accessible and has sufficient permissions")
106 )
107 )]
108 Storage,
109}
110
111impl ClientError {
112 pub fn new(kind: ClientErrorKind, source: Option<BoxError>) -> Self {
114 Self {
115 kind,
116 source,
117 help: None,
118 context: None,
119 url: None,
120 details: None,
121 location: None,
122 }
123 }
124
125 pub fn kind(&self) -> &ClientErrorKind {
127 &self.kind
128 }
129
130 pub fn source_err(&self) -> Option<&BoxError> {
132 self.source.as_ref()
133 }
134
135 pub fn context(&self) -> Option<&str> {
137 self.context.as_ref().map(|s| s.as_str())
138 }
139
140 pub fn url(&self) -> Option<&str> {
142 self.url.as_ref().map(|s| s.as_str())
143 }
144
145 pub fn details(&self) -> Option<&str> {
147 self.details.as_ref().map(|s| s.as_str())
148 }
149
150 pub fn location(&self) -> Option<&str> {
152 self.location.as_ref().map(|s| s.as_str())
153 }
154
155 pub fn with_help(mut self, help: impl Into<SmolStr>) -> Self {
157 self.help = Some(help.into());
158 self
159 }
160
161 pub fn with_context(mut self, context: impl Into<SmolStr>) -> Self {
163 self.context = Some(context.into());
164 self
165 }
166
167 pub fn with_url(mut self, url: impl Into<SmolStr>) -> Self {
169 self.url = Some(url.into());
170 self
171 }
172
173 pub fn with_details(mut self, details: impl Into<SmolStr>) -> Self {
175 self.details = Some(details.into());
176 self
177 }
178
179 pub fn with_location(mut self, location: impl Into<SmolStr>) -> Self {
181 self.location = Some(location.into());
182 self
183 }
184
185 pub fn append_context(mut self, additional: impl AsRef<str>) -> Self {
190 self.context = Some(match self.context.take() {
191 Some(existing) => smol_str::format_smolstr!("{}: {}", existing, additional.as_ref()),
192 None => additional.as_ref().into(),
193 });
194 self
195 }
196
197 pub fn for_nsid(self, nsid: &str) -> Self {
201 self.append_context(smol_str::format_smolstr!("[{}]", nsid))
202 }
203
204 pub fn for_collection(self, operation: &str, collection_nsid: &str) -> Self {
208 self.append_context(smol_str::format_smolstr!(
209 "{} [{}]",
210 operation,
211 collection_nsid
212 ))
213 }
214
215 pub fn transport(source: impl core::error::Error + Send + Sync + 'static) -> Self {
219 Self::new(ClientErrorKind::Transport, Some(Box::new(source)))
220 }
221
222 pub fn invalid_request(msg: impl Into<SmolStr>) -> Self {
224 Self::new(ClientErrorKind::InvalidRequest(msg.into()), None)
225 }
226
227 pub fn encode(msg: impl Into<SmolStr>) -> Self {
229 Self::new(ClientErrorKind::Encode(msg.into()), None)
230 }
231
232 pub fn decode(msg: impl Into<SmolStr>) -> Self {
234 Self::new(ClientErrorKind::Decode(msg.into()), None)
235 }
236
237 pub fn http(status: http::StatusCode, body: Option<Bytes>) -> Self {
239 let http_err = HttpError { status, body };
240 Self::new(ClientErrorKind::Http { status }, Some(Box::new(http_err)))
241 }
242
243 pub fn auth(auth_error: AuthError) -> Self {
245 Self::new(ClientErrorKind::Auth(auth_error), None)
246 }
247
248 pub fn identity_resolution(source: impl core::error::Error + Send + Sync + 'static) -> Self {
250 Self::new(ClientErrorKind::IdentityResolution, Some(Box::new(source)))
251 }
252
253 pub fn storage(source: impl core::error::Error + Send + Sync + 'static) -> Self {
255 Self::new(ClientErrorKind::Storage, Some(Box::new(source)))
256 }
257}
258
259pub type XrpcResult<T> = Result<T, ClientError>;
261
262#[derive(Debug, thiserror::Error)]
271#[cfg_attr(feature = "std", derive(Diagnostic))]
272#[non_exhaustive]
273pub enum DecodeError {
274 #[error("Failed to deserialize JSON: {0}")]
276 Json(
277 #[from]
278 #[source]
279 serde_json::Error,
280 ),
281 #[cfg(feature = "std")]
283 #[error("Failed to deserialize CBOR: {0}")]
284 CborLocal(
285 #[from]
286 #[source]
287 serde_ipld_dagcbor::DecodeError<std::io::Error>,
288 ),
289 #[error("Failed to deserialize CBOR: {0}")]
291 CborRemote(
292 #[from]
293 #[source]
294 serde_ipld_dagcbor::DecodeError<HttpError>,
295 ),
296 #[error("Failed to deserialize DAG-CBOR: {0}")]
298 DagCborInfallible(
299 #[from]
300 #[source]
301 serde_ipld_dagcbor::DecodeError<core::convert::Infallible>,
302 ),
303 #[cfg(all(feature = "websocket", feature = "std"))]
305 #[error("Failed to deserialize cbor header: {0}")]
306 CborHeader(
307 #[from]
308 #[source]
309 ciborium::de::Error<std::io::Error>,
310 ),
311
312 #[cfg(all(feature = "websocket", not(feature = "std")))]
314 #[error("Failed to deserialize cbor header: {0}")]
315 CborHeader(
316 #[from]
317 #[source]
318 ciborium::de::Error<core::convert::Infallible>,
319 ),
320
321 #[cfg(feature = "websocket")]
323 #[error("Unknown event type: {0}")]
324 UnknownEventType(smol_str::SmolStr),
325}
326
327#[derive(Debug, thiserror::Error)]
329#[cfg_attr(feature = "std", derive(Diagnostic))]
330pub struct HttpError {
331 pub status: http::StatusCode,
333 pub body: Option<Bytes>,
335}
336
337impl core::fmt::Display for HttpError {
338 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
339 write!(f, "HTTP {}", self.status)?;
340 if let Some(body) = &self.body {
341 if let Ok(s) = core::str::from_utf8(body) {
342 write!(f, ":\n{}", s)?;
343 }
344 }
345 Ok(())
346 }
347}
348
349#[derive(Debug, thiserror::Error)]
351#[cfg_attr(feature = "std", derive(Diagnostic))]
352#[non_exhaustive]
353pub enum AuthError {
354 #[error("Access token expired")]
356 TokenExpired,
357
358 #[error("Invalid access token")]
360 InvalidToken,
361
362 #[error("Token refresh failed")]
364 RefreshFailed,
365
366 #[error("No authentication provided, but endpoint requires auth")]
368 NotAuthenticated,
369
370 #[error("DPoP proof construction failed")]
372 DpopProofFailed,
373
374 #[error("DPoP nonce negotiation failed")]
376 DpopNonceFailed,
377
378 #[error("Authentication error: {0:?}")]
380 Other(http::HeaderValue),
381}
382
383impl crate::IntoStatic for AuthError {
384 type Output = AuthError;
385
386 fn into_static(self) -> Self::Output {
387 match self {
388 AuthError::TokenExpired => AuthError::TokenExpired,
389 AuthError::InvalidToken => AuthError::InvalidToken,
390 AuthError::RefreshFailed => AuthError::RefreshFailed,
391 AuthError::NotAuthenticated => AuthError::NotAuthenticated,
392 AuthError::DpopProofFailed => AuthError::DpopProofFailed,
393 AuthError::DpopNonceFailed => AuthError::DpopNonceFailed,
394 AuthError::Other(header) => AuthError::Other(header),
395 }
396 }
397}
398
399impl From<DecodeError> for ClientError {
404 fn from(e: DecodeError) -> Self {
405 let msg = smol_str::format_smolstr!("{:?}", e);
406 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
407 .with_context("response deserialization failed")
408 }
409}
410
411impl From<HttpError> for ClientError {
412 fn from(e: HttpError) -> Self {
413 Self::http(e.status, e.body)
414 }
415}
416
417impl From<AuthError> for ClientError {
418 fn from(e: AuthError) -> Self {
419 Self::auth(e)
420 }
421}
422
423impl From<EncodeError> for ClientError {
424 fn from(e: EncodeError) -> Self {
425 let msg = smol_str::format_smolstr!("{:?}", e);
426 Self::new(ClientErrorKind::Encode(msg), Some(Box::new(e)))
427 .with_context("request encoding failed")
428 }
429}
430
431#[cfg(feature = "reqwest-client")]
433impl From<reqwest::Error> for ClientError {
434 #[cfg(not(target_arch = "wasm32"))]
435 fn from(e: reqwest::Error) -> Self {
436 Self::transport(e)
437 }
438
439 #[cfg(target_arch = "wasm32")]
440 fn from(e: reqwest::Error) -> Self {
441 Self::transport(e)
442 }
443}
444
445impl From<serde_json::Error> for ClientError {
447 fn from(e: serde_json::Error) -> Self {
448 let msg = smol_str::format_smolstr!("{:?}", e);
449 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
450 .with_context("JSON deserialization failed")
451 }
452}
453
454#[cfg(feature = "std")]
455impl From<serde_ipld_dagcbor::DecodeError<std::io::Error>> for ClientError {
456 fn from(e: serde_ipld_dagcbor::DecodeError<std::io::Error>) -> Self {
457 let msg = smol_str::format_smolstr!("{:?}", e);
458 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
459 .with_context("DAG-CBOR deserialization failed (local I/O)")
460 }
461}
462
463impl From<serde_ipld_dagcbor::DecodeError<HttpError>> for ClientError {
464 fn from(e: serde_ipld_dagcbor::DecodeError<HttpError>) -> Self {
465 let msg = smol_str::format_smolstr!("{:?}", e);
466 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
467 .with_context("DAG-CBOR deserialization failed (remote)")
468 }
469}
470
471impl From<serde_ipld_dagcbor::DecodeError<core::convert::Infallible>> for ClientError {
472 fn from(e: serde_ipld_dagcbor::DecodeError<core::convert::Infallible>) -> Self {
473 let msg = smol_str::format_smolstr!("{:?}", e);
474 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
475 .with_context("DAG-CBOR deserialization failed (in-memory)")
476 }
477}
478
479#[cfg(all(feature = "websocket", feature = "std"))]
480impl From<ciborium::de::Error<std::io::Error>> for ClientError {
481 fn from(e: ciborium::de::Error<std::io::Error>) -> Self {
482 let msg = smol_str::format_smolstr!("{:?}", e);
483 Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
484 .with_context("CBOR header deserialization failed")
485 }
486}
487
488impl From<crate::session::SessionStoreError> for ClientError {
490 fn from(e: crate::session::SessionStoreError) -> Self {
491 Self::storage(e)
492 }
493}
494
495impl From<crate::deps::fluent_uri::ParseError> for ClientError {
497 fn from(e: crate::deps::fluent_uri::ParseError) -> Self {
498 Self::invalid_request(e.to_string())
499 }
500}