log client errors with better context #4

merged
opened by nekomimi.pet targeting main

i needed to get better error logging for requests made as a generic client error from what i could tell is something i was unable to improve as a user of a library

im not sure about the custom display implementation, that code could be improved.

looks like this on a 429 × client error (see context for details): HTTP error 429 Too Many Requests ╰─▶ jacquard::agent::client

Changed files
+89 -5
crates
jacquard
src
client
+89 -5
crates/jacquard/src/client/error.rs
··· 1 - use jacquard_common::error::{AuthError, ClientError}; 1 + use jacquard_common::error::{AuthError, ClientError, ClientErrorKind}; 2 2 use jacquard_common::types::did::Did; 3 3 use jacquard_common::types::nsid::Nsid; 4 4 use jacquard_common::types::string::{RecordKey, Rkey}; ··· 11 11 12 12 /// Error type for Agent convenience methods 13 13 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 14 - #[error("{kind}")] 15 14 pub struct AgentError { 16 15 #[diagnostic_source] 17 16 kind: AgentErrorKind, ··· 26 25 xrpc: Option<Data<'static>>, 27 26 } 28 27 28 + impl std::fmt::Display for AgentError { 29 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 + write!(f, "{}", self.kind)?; 31 + 32 + // Add context if available 33 + if let Some(context) = &self.context { 34 + write!(f, ": {}", context)?; 35 + } 36 + 37 + // Add URL if available 38 + if let Some(url) = &self.url { 39 + write!(f, " (url: {})", url)?; 40 + } 41 + 42 + // Add details if available 43 + if let Some(details) = &self.details { 44 + write!(f, " [{}]", details)?; 45 + } 46 + 47 + Ok(()) 48 + } 49 + } 50 + 29 51 /// Error categories for Agent operations 30 52 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 31 53 pub enum AgentErrorKind { 32 54 /// Transport/network layer failure 33 - #[error("client error")] 34 - #[diagnostic(code(jacquard::agent::client))] 55 + #[error("client error (see context for details)")] 56 + #[diagnostic( 57 + code(jacquard::agent::client), 58 + help("check source error and context for specific failure details") 59 + )] 35 60 Client, 36 61 37 62 /// No session available for operations requiring authentication ··· 226 251 227 252 impl From<ClientError> for AgentError { 228 253 fn from(e: ClientError) -> Self { 229 - Self::new(AgentErrorKind::Client, Some(Box::new(e))) 254 + use smol_str::ToSmolStr; 255 + 256 + let context_msg: SmolStr; 257 + let help_msg: SmolStr; 258 + let url = e.url().map(|s| s.to_smolstr()); 259 + let details = e.details().map(|s| s.to_smolstr()); 260 + 261 + // Build context and help based on the error kind 262 + match e.kind() { 263 + ClientErrorKind::Transport => { 264 + help_msg = "check network connectivity and server availability".to_smolstr(); 265 + context_msg = "network/transport error during request".to_smolstr(); 266 + } 267 + ClientErrorKind::InvalidRequest(msg) => { 268 + help_msg = "verify request parameters are valid".to_smolstr(); 269 + context_msg = smol_str::format_smolstr!("invalid request: {}", msg); 270 + } 271 + ClientErrorKind::Encode(msg) => { 272 + help_msg = "check request body format".to_smolstr(); 273 + context_msg = smol_str::format_smolstr!("failed to encode request: {}", msg); 274 + } 275 + ClientErrorKind::Decode(msg) => { 276 + help_msg = "server returned unexpected response format".to_smolstr(); 277 + context_msg = smol_str::format_smolstr!("failed to decode response: {}", msg); 278 + } 279 + ClientErrorKind::Http { status } => { 280 + help_msg = match status.as_u16() { 281 + 400..=499 => "check request parameters and authentication", 282 + 500..=599 => "server error - try again later or check server logs", 283 + _ => "unexpected HTTP status code", 284 + } 285 + .to_smolstr(); 286 + context_msg = smol_str::format_smolstr!("HTTP error {}", status); 287 + } 288 + ClientErrorKind::Auth(auth_err) => { 289 + help_msg = "verify authentication credentials and session".to_smolstr(); 290 + context_msg = smol_str::format_smolstr!("authentication error: {}", auth_err); 291 + } 292 + ClientErrorKind::IdentityResolution => { 293 + help_msg = "check handle/DID is valid and resolvable".to_smolstr(); 294 + context_msg = "identity resolution failed".to_smolstr(); 295 + } 296 + ClientErrorKind::Storage => { 297 + help_msg = "verify storage backend is accessible".to_smolstr(); 298 + context_msg = "storage operation failed".to_smolstr(); 299 + } 300 + } 301 + 302 + let mut error = Self::new(AgentErrorKind::Client, Some(Box::new(e))); 303 + error = error.with_context(context_msg); 304 + error = error.with_help(help_msg); 305 + 306 + if let Some(url) = url { 307 + error = error.with_url(url); 308 + } 309 + if let Some(details) = details { 310 + error = error.with_details(details); 311 + } 312 + 313 + error 230 314 } 231 315 } 232 316