//! Error types for XRPC client operations use crate::xrpc::EncodeError; use alloc::boxed::Box; use alloc::string::ToString; use bytes::Bytes; use smol_str::SmolStr; #[cfg(feature = "std")] use miette::Diagnostic; /// Boxed error type for wrapping arbitrary errors pub type BoxError = Box; /// Client error type for all XRPC client operations #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "std", derive(Diagnostic))] #[error("{kind}")] pub struct ClientError { #[cfg_attr(feature = "std", diagnostic_source)] kind: ClientErrorKind, #[source] source: Option, #[cfg_attr(feature = "std", help)] help: Option, context: Option, url: Option, details: Option, location: Option, } /// Error categories for client operations #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "std", derive(Diagnostic))] #[non_exhaustive] pub enum ClientErrorKind { /// HTTP transport error (connection, timeout, etc.) #[error("transport error")] #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::transport)))] Transport, /// Request validation/construction failed #[error("invalid request: {0}")] #[cfg_attr(feature = "std", diagnostic( code(jacquard::client::invalid_request), help("check request parameters and format") ))] InvalidRequest(SmolStr), /// Request serialization failed #[error("encode error: {0}")] #[cfg_attr(feature = "std", diagnostic( code(jacquard::client::encode), help("check request body format and encoding") ))] Encode(SmolStr), /// Response deserialization failed #[error("decode error: {0}")] #[cfg_attr(feature = "std", diagnostic( code(jacquard::client::decode), help("check response format and encoding") ))] Decode(SmolStr), /// HTTP error response (non-200 status) #[error("HTTP {status}")] #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::http)))] Http { /// HTTP status code status: http::StatusCode, }, /// Authentication/authorization error #[error("auth error: {0}")] #[cfg_attr(feature = "std", diagnostic(code(jacquard::client::auth)))] Auth(AuthError), /// Identity resolution error (handle→DID, DID→Doc) #[error("identity resolution failed")] #[cfg_attr(feature = "std", diagnostic( code(jacquard::client::identity_resolution), help("check handle/DID is valid and network is accessible") ))] IdentityResolution, /// Storage/persistence error #[error("storage error")] #[cfg_attr(feature = "std", diagnostic( code(jacquard::client::storage), help("check storage backend is accessible and has sufficient permissions") ))] Storage, } impl ClientError { /// Create a new error with the given kind and optional source pub fn new(kind: ClientErrorKind, source: Option) -> Self { Self { kind, source, help: None, context: None, url: None, details: None, location: None, } } /// Get the error kind pub fn kind(&self) -> &ClientErrorKind { &self.kind } /// Get the source error if present pub fn source_err(&self) -> Option<&BoxError> { self.source.as_ref() } /// Get the context string if present pub fn context(&self) -> Option<&str> { self.context.as_ref().map(|s| s.as_str()) } /// Get the URL if present pub fn url(&self) -> Option<&str> { self.url.as_ref().map(|s| s.as_str()) } /// Get the details if present pub fn details(&self) -> Option<&str> { self.details.as_ref().map(|s| s.as_str()) } /// Get the location if present pub fn location(&self) -> Option<&str> { self.location.as_ref().map(|s| s.as_str()) } /// Add help text to this error pub fn with_help(mut self, help: impl Into) -> Self { self.help = Some(help.into()); self } /// Add context to this error pub fn with_context(mut self, context: impl Into) -> Self { self.context = Some(context.into()); self } /// Add URL to this error pub fn with_url(mut self, url: impl Into) -> Self { self.url = Some(url.into()); self } /// Add details to this error pub fn with_details(mut self, details: impl Into) -> Self { self.details = Some(details.into()); self } /// Add location to this error pub fn with_location(mut self, location: impl Into) -> Self { self.location = Some(location.into()); self } /// Append additional context to existing context string. /// /// If context already exists, appends with ": " separator. /// If no context exists, sets it directly. pub fn append_context(mut self, additional: impl AsRef) -> Self { self.context = Some(match self.context.take() { Some(existing) => smol_str::format_smolstr!("{}: {}", existing, additional.as_ref()), None => additional.as_ref().into(), }); self } /// Add NSID context for XRPC operations. /// /// Appends the NSID in brackets to existing context, e.g. `"network timeout: [com.atproto.repo.getRecord]"`. pub fn for_nsid(self, nsid: &str) -> Self { self.append_context(smol_str::format_smolstr!("[{}]", nsid)) } /// Add collection context for record operations. /// /// Use this when a record operation fails to indicate the target collection. pub fn for_collection(self, operation: &str, collection_nsid: &str) -> Self { self.append_context(smol_str::format_smolstr!("{} [{}]", operation, collection_nsid)) } // Constructors for each kind /// Create a transport error pub fn transport(source: impl core::error::Error + Send + Sync + 'static) -> Self { Self::new(ClientErrorKind::Transport, Some(Box::new(source))) } /// Create an invalid request error pub fn invalid_request(msg: impl Into) -> Self { Self::new(ClientErrorKind::InvalidRequest(msg.into()), None) } /// Create an encode error pub fn encode(msg: impl Into) -> Self { Self::new(ClientErrorKind::Encode(msg.into()), None) } /// Create a decode error pub fn decode(msg: impl Into) -> Self { Self::new(ClientErrorKind::Decode(msg.into()), None) } /// Create an HTTP error with status code and optional body pub fn http(status: http::StatusCode, body: Option) -> Self { let http_err = HttpError { status, body }; Self::new(ClientErrorKind::Http { status }, Some(Box::new(http_err))) } /// Create an authentication error pub fn auth(auth_error: AuthError) -> Self { Self::new(ClientErrorKind::Auth(auth_error), None) } /// Create an identity resolution error pub fn identity_resolution(source: impl core::error::Error + Send + Sync + 'static) -> Self { Self::new(ClientErrorKind::IdentityResolution, Some(Box::new(source))) } /// Create a storage error pub fn storage(source: impl core::error::Error + Send + Sync + 'static) -> Self { Self::new(ClientErrorKind::Storage, Some(Box::new(source))) } } /// Result type for client operations pub type XrpcResult = Result; // ============================================================================ // Old error types (deprecated) // ============================================================================ /// Response deserialization errors /// /// Preserves detailed error information from various deserialization backends. /// Can be converted to string for serialization while maintaining the full error context. #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "std", derive(Diagnostic))] #[non_exhaustive] pub enum DecodeError { /// JSON deserialization failed #[error("Failed to deserialize JSON: {0}")] Json( #[from] #[source] serde_json::Error, ), /// CBOR deserialization failed (local I/O) #[cfg(feature = "std")] #[error("Failed to deserialize CBOR: {0}")] CborLocal( #[from] #[source] serde_ipld_dagcbor::DecodeError, ), /// CBOR deserialization failed (remote/reqwest) #[error("Failed to deserialize CBOR: {0}")] CborRemote( #[from] #[source] serde_ipld_dagcbor::DecodeError, ), /// DAG-CBOR deserialization failed (in-memory, e.g., WebSocket frames) #[error("Failed to deserialize DAG-CBOR: {0}")] DagCborInfallible( #[from] #[source] serde_ipld_dagcbor::DecodeError, ), /// CBOR header deserialization failed (framed WebSocket messages) #[cfg(all(feature = "websocket", feature = "std"))] #[error("Failed to deserialize cbor header: {0}")] CborHeader( #[from] #[source] ciborium::de::Error, ), /// CBOR header deserialization failed (framed WebSocket messages, no_std) #[cfg(all(feature = "websocket", not(feature = "std")))] #[error("Failed to deserialize cbor header: {0}")] CborHeader( #[from] #[source] ciborium::de::Error, ), /// Unknown event type in framed message #[cfg(feature = "websocket")] #[error("Unknown event type: {0}")] UnknownEventType(smol_str::SmolStr), } /// HTTP error response (non-200 status codes outside of XRPC error handling) #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "std", derive(Diagnostic))] pub struct HttpError { /// HTTP status code pub status: http::StatusCode, /// Response body if available pub body: Option, } impl core::fmt::Display for HttpError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "HTTP {}", self.status)?; if let Some(body) = &self.body { if let Ok(s) = core::str::from_utf8(body) { write!(f, ":\n{}", s)?; } } Ok(()) } } /// Authentication and authorization errors #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "std", derive(Diagnostic))] #[non_exhaustive] pub enum AuthError { /// Access token has expired (use refresh token to get a new one) #[error("Access token expired")] TokenExpired, /// Access token is invalid or malformed #[error("Invalid access token")] InvalidToken, /// Token refresh request failed #[error("Token refresh failed")] RefreshFailed, /// Request requires authentication but none was provided #[error("No authentication provided, but endpoint requires auth")] NotAuthenticated, /// DPoP proof construction failed (key or signing issue) #[error("DPoP proof construction failed")] DpopProofFailed, /// DPoP nonce retry failed (server rejected proof even after nonce update) #[error("DPoP nonce negotiation failed")] DpopNonceFailed, /// Other authentication error #[error("Authentication error: {0:?}")] Other(http::HeaderValue), } impl crate::IntoStatic for AuthError { type Output = AuthError; fn into_static(self) -> Self::Output { match self { AuthError::TokenExpired => AuthError::TokenExpired, AuthError::InvalidToken => AuthError::InvalidToken, AuthError::RefreshFailed => AuthError::RefreshFailed, AuthError::NotAuthenticated => AuthError::NotAuthenticated, AuthError::DpopProofFailed => AuthError::DpopProofFailed, AuthError::DpopNonceFailed => AuthError::DpopNonceFailed, AuthError::Other(header) => AuthError::Other(header), } } } // ============================================================================ // Conversions from old to new // ============================================================================ impl From for ClientError { fn from(e: DecodeError) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("response deserialization failed") } } impl From for ClientError { fn from(e: HttpError) -> Self { Self::http(e.status, e.body) } } impl From for ClientError { fn from(e: AuthError) -> Self { Self::auth(e) } } impl From for ClientError { fn from(e: EncodeError) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Encode(msg), Some(Box::new(e))) .with_context("request encoding failed") } } // Platform-specific conversions #[cfg(feature = "reqwest-client")] impl From for ClientError { #[cfg(not(target_arch = "wasm32"))] fn from(e: reqwest::Error) -> Self { Self::transport(e) } #[cfg(target_arch = "wasm32")] fn from(e: reqwest::Error) -> Self { Self::transport(e) } } // Serde error conversions impl From for ClientError { fn from(e: serde_json::Error) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("JSON deserialization failed") } } #[cfg(feature = "std")] impl From> for ClientError { fn from(e: serde_ipld_dagcbor::DecodeError) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("DAG-CBOR deserialization failed (local I/O)") } } impl From> for ClientError { fn from(e: serde_ipld_dagcbor::DecodeError) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("DAG-CBOR deserialization failed (remote)") } } impl From> for ClientError { fn from(e: serde_ipld_dagcbor::DecodeError) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("DAG-CBOR deserialization failed (in-memory)") } } #[cfg(all(feature = "websocket", feature = "std"))] impl From> for ClientError { fn from(e: ciborium::de::Error) -> Self { let msg = smol_str::format_smolstr!("{:?}", e); Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e))) .with_context("CBOR header deserialization failed") } } // Session store errors impl From for ClientError { fn from(e: crate::session::SessionStoreError) -> Self { Self::storage(e) } } // URL parse errors impl From for ClientError { fn from(e: url::ParseError) -> Self { Self::invalid_request(e.to_string()) } }