A better Rust ATProto crate
102
fork

Configure Feed

Select the types of activity you want to include in your feed.

at pretty-codegen 500 lines 16 kB view raw
1//! Error types for XRPC client operations 2 3use 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 12/// Boxed error type for wrapping arbitrary errors 13pub type BoxError = Box<dyn core::error::Error + Send + Sync + 'static>; 14 15/// Client error type for all XRPC client operations 16#[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/// Error categories for client operations 33#[derive(Debug, thiserror::Error)] 34#[cfg_attr(feature = "std", derive(Diagnostic))] 35#[non_exhaustive] 36pub enum ClientErrorKind { 37 /// HTTP transport error (connection, timeout, etc.) 38 #[error("transport error")] 39 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::transport)))] 40 Transport, 41 42 /// Request validation/construction failed 43 #[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 /// Request serialization failed 54 #[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 /// Response deserialization failed 65 #[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 /// HTTP error response (non-200 status) 76 #[error("HTTP {status}")] 77 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::http)))] 78 Http { 79 /// HTTP status code 80 status: http::StatusCode, 81 }, 82 83 /// Authentication/authorization error 84 #[error("auth error: {0}")] 85 #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::auth)))] 86 Auth(AuthError), 87 88 /// Identity resolution error (handle→DID, DID→Doc) 89 #[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 /// Storage/persistence error 100 #[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 /// Create a new error with the given kind and optional source 113 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 /// Get the error kind 126 pub fn kind(&self) -> &ClientErrorKind { 127 &self.kind 128 } 129 130 /// Get the source error if present 131 pub fn source_err(&self) -> Option<&BoxError> { 132 self.source.as_ref() 133 } 134 135 /// Get the context string if present 136 pub fn context(&self) -> Option<&str> { 137 self.context.as_ref().map(|s| s.as_str()) 138 } 139 140 /// Get the URL if present 141 pub fn url(&self) -> Option<&str> { 142 self.url.as_ref().map(|s| s.as_str()) 143 } 144 145 /// Get the details if present 146 pub fn details(&self) -> Option<&str> { 147 self.details.as_ref().map(|s| s.as_str()) 148 } 149 150 /// Get the location if present 151 pub fn location(&self) -> Option<&str> { 152 self.location.as_ref().map(|s| s.as_str()) 153 } 154 155 /// Add help text to this error 156 pub fn with_help(mut self, help: impl Into<SmolStr>) -> Self { 157 self.help = Some(help.into()); 158 self 159 } 160 161 /// Add context to this error 162 pub fn with_context(mut self, context: impl Into<SmolStr>) -> Self { 163 self.context = Some(context.into()); 164 self 165 } 166 167 /// Add URL to this error 168 pub fn with_url(mut self, url: impl Into<SmolStr>) -> Self { 169 self.url = Some(url.into()); 170 self 171 } 172 173 /// Add details to this error 174 pub fn with_details(mut self, details: impl Into<SmolStr>) -> Self { 175 self.details = Some(details.into()); 176 self 177 } 178 179 /// Add location to this error 180 pub fn with_location(mut self, location: impl Into<SmolStr>) -> Self { 181 self.location = Some(location.into()); 182 self 183 } 184 185 /// Append additional context to existing context string. 186 /// 187 /// If context already exists, appends with ": " separator. 188 /// If no context exists, sets it directly. 189 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 /// Add NSID context for XRPC operations. 198 /// 199 /// Appends the NSID in brackets to existing context, e.g. `"network timeout: [com.atproto.repo.getRecord]"`. 200 pub fn for_nsid(self, nsid: &str) -> Self { 201 self.append_context(smol_str::format_smolstr!("[{}]", nsid)) 202 } 203 204 /// Add collection context for record operations. 205 /// 206 /// Use this when a record operation fails to indicate the target collection. 207 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 // Constructors for each kind 216 217 /// Create a transport error 218 pub fn transport(source: impl core::error::Error + Send + Sync + 'static) -> Self { 219 Self::new(ClientErrorKind::Transport, Some(Box::new(source))) 220 } 221 222 /// Create an invalid request error 223 pub fn invalid_request(msg: impl Into<SmolStr>) -> Self { 224 Self::new(ClientErrorKind::InvalidRequest(msg.into()), None) 225 } 226 227 /// Create an encode error 228 pub fn encode(msg: impl Into<SmolStr>) -> Self { 229 Self::new(ClientErrorKind::Encode(msg.into()), None) 230 } 231 232 /// Create a decode error 233 pub fn decode(msg: impl Into<SmolStr>) -> Self { 234 Self::new(ClientErrorKind::Decode(msg.into()), None) 235 } 236 237 /// Create an HTTP error with status code and optional body 238 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 /// Create an authentication error 244 pub fn auth(auth_error: AuthError) -> Self { 245 Self::new(ClientErrorKind::Auth(auth_error), None) 246 } 247 248 /// Create an identity resolution error 249 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 /// Create a storage error 254 pub fn storage(source: impl core::error::Error + Send + Sync + 'static) -> Self { 255 Self::new(ClientErrorKind::Storage, Some(Box::new(source))) 256 } 257} 258 259/// Result type for client operations 260pub type XrpcResult<T> = Result<T, ClientError>; 261 262// ============================================================================ 263// Old error types (deprecated) 264// ============================================================================ 265 266/// Response deserialization errors 267/// 268/// Preserves detailed error information from various deserialization backends. 269/// Can be converted to string for serialization while maintaining the full error context. 270#[derive(Debug, thiserror::Error)] 271#[cfg_attr(feature = "std", derive(Diagnostic))] 272#[non_exhaustive] 273pub enum DecodeError { 274 /// JSON deserialization failed 275 #[error("Failed to deserialize JSON: {0}")] 276 Json( 277 #[from] 278 #[source] 279 serde_json::Error, 280 ), 281 /// CBOR deserialization failed (local I/O) 282 #[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 /// CBOR deserialization failed (remote/reqwest) 290 #[error("Failed to deserialize CBOR: {0}")] 291 CborRemote( 292 #[from] 293 #[source] 294 serde_ipld_dagcbor::DecodeError<HttpError>, 295 ), 296 /// DAG-CBOR deserialization failed (in-memory, e.g., WebSocket frames) 297 #[error("Failed to deserialize DAG-CBOR: {0}")] 298 DagCborInfallible( 299 #[from] 300 #[source] 301 serde_ipld_dagcbor::DecodeError<core::convert::Infallible>, 302 ), 303 /// CBOR header deserialization failed (framed WebSocket messages) 304 #[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 /// CBOR header deserialization failed (framed WebSocket messages, no_std) 313 #[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 /// Unknown event type in framed message 322 #[cfg(feature = "websocket")] 323 #[error("Unknown event type: {0}")] 324 UnknownEventType(smol_str::SmolStr), 325} 326 327/// HTTP error response (non-200 status codes outside of XRPC error handling) 328#[derive(Debug, thiserror::Error)] 329#[cfg_attr(feature = "std", derive(Diagnostic))] 330pub struct HttpError { 331 /// HTTP status code 332 pub status: http::StatusCode, 333 /// Response body if available 334 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/// Authentication and authorization errors 350#[derive(Debug, thiserror::Error)] 351#[cfg_attr(feature = "std", derive(Diagnostic))] 352#[non_exhaustive] 353pub enum AuthError { 354 /// Access token has expired (use refresh token to get a new one) 355 #[error("Access token expired")] 356 TokenExpired, 357 358 /// Access token is invalid or malformed 359 #[error("Invalid access token")] 360 InvalidToken, 361 362 /// Token refresh request failed 363 #[error("Token refresh failed")] 364 RefreshFailed, 365 366 /// Request requires authentication but none was provided 367 #[error("No authentication provided, but endpoint requires auth")] 368 NotAuthenticated, 369 370 /// DPoP proof construction failed (key or signing issue) 371 #[error("DPoP proof construction failed")] 372 DpopProofFailed, 373 374 /// DPoP nonce retry failed (server rejected proof even after nonce update) 375 #[error("DPoP nonce negotiation failed")] 376 DpopNonceFailed, 377 378 /// Other authentication error 379 #[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 399// ============================================================================ 400// Conversions from old to new 401// ============================================================================ 402 403impl 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// Platform-specific conversions 432#[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 445// Serde error conversions 446impl 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 488// Session store errors 489impl From<crate::session::SessionStoreError> for ClientError { 490 fn from(e: crate::session::SessionStoreError) -> Self { 491 Self::storage(e) 492 } 493} 494 495// fluent_uri parse errors 496impl 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}