A better Rust ATProto crate
at main 4.6 kB view raw
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}