server/common/errors.rs
1use thiserror::Error;
2
3/// HTTP-related errors with detailed context for network operations.
4///
5/// This enum provides comprehensive error classification for HTTP operations
6/// throughout the application, including Azure API calls, authentication requests,
7/// and other network operations. Each error variant includes relevant context
8/// to aid in debugging and error handling.
9///
10/// # Error Categories
11///
12/// ## Client Configuration Errors
13/// - [`ClientCreation`] - HTTP client initialization failures
14///
15/// ## Request Execution Errors
16/// - [`RequestFailed`] - General request failures with URL and reason
17/// - [`Timeout`] - Request timeout with duration and target URL
18/// - [`InvalidResponse`] - Unexpected response format or content
19///
20/// ## Rate Limiting and Service Errors
21/// - [`RateLimited`] - Rate limiting with retry timing information
22///
23/// # Examples
24///
25/// ## Basic Error Handling
26/// ```no_run
27/// use quetty_server::common::errors::HttpError;
28///
29/// async fn handle_http_error(error: HttpError) {
30/// match error {
31/// HttpError::Timeout { url, seconds } => {
32/// eprintln!("Request to {} timed out after {}s", url, seconds);
33/// // Implement retry with longer timeout
34/// }
35/// HttpError::RateLimited { retry_after_seconds } => {
36/// println!("Rate limited. Retrying after {}s", retry_after_seconds);
37/// // Wait and retry
38/// }
39/// HttpError::RequestFailed { url, reason } => {
40/// eprintln!("Request to {} failed: {}", url, reason);
41/// // Log and handle specific failure
42/// }
43/// HttpError::ClientCreation { reason } => {
44/// eprintln!("Failed to create HTTP client: {}", reason);
45/// // Reinitialize client with different configuration
46/// }
47/// HttpError::InvalidResponse { expected, actual } => {
48/// eprintln!("Invalid response: expected {}, got {}", expected, actual);
49/// // Handle unexpected response format
50/// }
51/// }
52/// }
53/// ```
54///
55/// ## Retry Logic Implementation
56/// ```no_run
57/// use quetty_server::common::errors::HttpError;
58/// use std::time::Duration;
59/// use tokio::time::sleep;
60///
61/// async fn http_request_with_retry<T>(
62/// request_fn: impl Fn() -> Result<T, HttpError>
63/// ) -> Result<T, HttpError> {
64/// let mut attempts = 0;
65/// let max_attempts = 3;
66///
67/// loop {
68/// attempts += 1;
69///
70/// match request_fn() {
71/// Ok(result) => return Ok(result),
72/// Err(HttpError::RateLimited { retry_after_seconds }) => {
73/// if attempts < max_attempts {
74/// sleep(Duration::from_secs(retry_after_seconds)).await;
75/// continue;
76/// }
77/// return Err(HttpError::RateLimited { retry_after_seconds });
78/// }
79/// Err(HttpError::Timeout { url, seconds }) => {
80/// if attempts < max_attempts {
81/// // Exponential backoff for timeouts
82/// sleep(Duration::from_secs(2_u64.pow(attempts))).await;
83/// continue;
84/// }
85/// return Err(HttpError::Timeout { url, seconds });
86/// }
87/// Err(other) => return Err(other), // Don't retry client errors
88/// }
89/// }
90/// }
91/// ```
92///
93/// ## Azure API Error Handling
94/// ```no_run
95/// use quetty_server::common::errors::HttpError;
96///
97/// async fn call_azure_api(endpoint: &str) -> Result<String, HttpError> {
98/// // Simulated Azure API call
99/// match make_request(endpoint).await {
100/// Ok(response) => Ok(response),
101/// Err(e) => {
102/// // Convert to structured HttpError
103/// Err(HttpError::RequestFailed {
104/// url: endpoint.to_string(),
105/// reason: e.to_string(),
106/// })
107/// }
108/// }
109/// }
110///
111/// // Usage with error context
112/// let result = call_azure_api("https://management.azure.com/subscriptions").await;
113/// match result {
114/// Ok(data) => println!("API call successful: {}", data),
115/// Err(HttpError::RequestFailed { url, reason }) => {
116/// if reason.contains("401") {
117/// // Handle authentication error
118/// println!("Authentication required for {}", url);
119/// } else if reason.contains("404") {
120/// // Handle resource not found
121/// println!("Resource not found: {}", url);
122/// } else {
123/// // Handle other errors
124/// println!("Request failed: {} - {}", url, reason);
125/// }
126/// }
127/// Err(other) => {
128/// println!("HTTP error: {}", other);
129/// }
130/// }
131/// ```
132///
133/// # Integration Patterns
134///
135/// ## Error Conversion
136/// This error type is designed to be easily converted to higher-level error types:
137///
138/// ```no_run
139/// use quetty_server::common::errors::HttpError;
140/// use quetty_server::service_bus_manager::ServiceBusError;
141///
142/// impl From<HttpError> for ServiceBusError {
143/// fn from(http_error: HttpError) -> Self {
144/// match http_error {
145/// HttpError::Timeout { .. } => ServiceBusError::OperationTimeout(http_error.to_string()),
146/// HttpError::RateLimited { .. } => ServiceBusError::OperationTimeout(http_error.to_string()),
147/// _ => ServiceBusError::ConnectionFailed(http_error.to_string()),
148/// }
149/// }
150/// }
151/// ```
152///
153/// ## Logging Integration
154/// ```no_run
155/// use quetty_server::common::errors::HttpError;
156///
157/// fn log_http_error(error: &HttpError) {
158/// match error {
159/// HttpError::RequestFailed { url, reason } => {
160/// log::error!("HTTP request failed: url={}, reason={}", url, reason);
161/// }
162/// HttpError::Timeout { url, seconds } => {
163/// log::warn!("HTTP request timeout: url={}, duration={}s", url, seconds);
164/// }
165/// HttpError::RateLimited { retry_after_seconds } => {
166/// log::info!("HTTP rate limited: retry_after={}s", retry_after_seconds);
167/// }
168/// _ => {
169/// log::error!("HTTP error: {}", error);
170/// }
171/// }
172/// }
173/// ```
174///
175/// [`ClientCreation`]: HttpError::ClientCreation
176/// [`RequestFailed`]: HttpError::RequestFailed
177/// [`Timeout`]: HttpError::Timeout
178/// [`InvalidResponse`]: HttpError::InvalidResponse
179/// [`RateLimited`]: HttpError::RateLimited
180#[derive(Debug, Error)]
181pub enum HttpError {
182 /// HTTP client initialization failed.
183 ///
184 /// This error occurs when creating or configuring the HTTP client fails,
185 /// typically due to invalid configuration, SSL/TLS setup issues, or
186 /// system resource constraints.
187 ///
188 /// # Fields
189 /// - `reason`: Detailed description of the client creation failure
190 ///
191 /// # Recovery
192 /// - Validate HTTP client configuration
193 /// - Check system resources and network settings
194 /// - Retry with alternative client configuration
195 #[error("HTTP client creation failed: {reason}")]
196 ClientCreation { reason: String },
197
198 /// HTTP request execution failed.
199 ///
200 /// This is a general request failure that can occur due to various
201 /// reasons including network issues, server errors, authentication
202 /// problems, or malformed requests.
203 ///
204 /// # Fields
205 /// - `url`: The URL that was being requested
206 /// - `reason`: Detailed description of the failure
207 ///
208 /// # Recovery
209 /// - Check network connectivity
210 /// - Validate request parameters and authentication
211 /// - Implement retry logic for transient failures
212 #[error("Request failed: {url} - {reason}")]
213 RequestFailed { url: String, reason: String },
214
215 /// HTTP request timed out.
216 ///
217 /// This error occurs when a request takes longer than the configured
218 /// timeout duration. This can happen due to slow network conditions,
219 /// overloaded servers, or network connectivity issues.
220 ///
221 /// # Fields
222 /// - `url`: The URL that timed out
223 /// - `seconds`: The timeout duration that was exceeded
224 ///
225 /// # Recovery
226 /// - Retry with longer timeout
227 /// - Check network connectivity
228 /// - Consider alternative endpoints if available
229 #[error("Request timeout after {seconds}s: {url}")]
230 Timeout { url: String, seconds: u64 },
231
232 /// Rate limiting is active for HTTP requests.
233 ///
234 /// This error occurs when the server has rate-limited the client
235 /// due to too many requests in a short period. The server provides
236 /// guidance on when to retry.
237 ///
238 /// # Fields
239 /// - `retry_after_seconds`: Duration to wait before retrying
240 ///
241 /// # Recovery
242 /// - Wait for the specified duration before retrying
243 /// - Implement request throttling to prevent future rate limiting
244 /// - Consider using exponential backoff for subsequent requests
245 #[error("Rate limit exceeded: retry after {retry_after_seconds}s")]
246 RateLimited { retry_after_seconds: u64 },
247
248 /// Received response doesn't match expected format.
249 ///
250 /// This error occurs when the server returns a response that doesn't
251 /// match the expected format, content type, or structure. This can
252 /// indicate API changes, server errors, or client-side parsing issues.
253 ///
254 /// # Fields
255 /// - `expected`: Description of what was expected
256 /// - `actual`: Description of what was actually received
257 ///
258 /// # Recovery
259 /// - Validate API endpoint and version compatibility
260 /// - Check response parsing logic
261 /// - Consider graceful degradation for unexpected responses
262 #[error("Invalid response: expected {expected}, got {actual}")]
263 InvalidResponse { expected: String, actual: String },
264}
265
266/// Cache-related errors for token and data caching operations.
267///
268/// This enum provides detailed error classification for caching operations
269/// throughout the application, particularly for authentication token caching
270/// and other temporary data storage. Each error variant includes relevant
271/// context to aid in cache management and error recovery.
272///
273/// # Error Categories
274///
275/// ## Cache Entry Lifecycle Errors
276/// - [`Expired`] - Cache entry has exceeded its time-to-live
277/// - [`Miss`] - Requested cache entry doesn't exist
278///
279/// ## Cache Capacity and Management Errors
280/// - [`Full`] - Cache has reached capacity limits
281/// - [`OperationFailed`] - General cache operation failures
282///
283/// # Examples
284///
285/// ## Token Cache Error Handling
286/// ```no_run
287/// use quetty_server::common::errors::CacheError;
288///
289/// async fn handle_token_cache_error(error: CacheError, token_key: &str) {
290/// match error {
291/// CacheError::Expired { key } => {
292/// println!("Token expired for key: {}", key);
293/// // Trigger token refresh
294/// refresh_token(&key).await;
295/// }
296/// CacheError::Miss { key } => {
297/// println!("Token not found in cache: {}", key);
298/// // Authenticate and cache new token
299/// authenticate_and_cache(&key).await;
300/// }
301/// CacheError::Full { key } => {
302/// println!("Cache full, cannot store token for: {}", key);
303/// // Implement cache eviction strategy
304/// evict_oldest_entries().await;
305/// retry_cache_operation(&key).await;
306/// }
307/// CacheError::OperationFailed { reason } => {
308/// eprintln!("Cache operation failed: {}", reason);
309/// // Log error and use alternative storage
310/// fallback_to_memory_cache(&token_key).await;
311/// }
312/// }
313/// }
314/// ```
315///
316/// ## Cache Management Patterns
317/// ```no_run
318/// use quetty_server::common::errors::CacheError;
319///
320/// async fn get_or_create_cached_item<T>(
321/// cache_key: &str,
322/// create_fn: impl Fn() -> Result<T, String>
323/// ) -> Result<T, CacheError> {
324/// // Try to get from cache first
325/// match get_from_cache(cache_key).await {
326/// Ok(item) => Ok(item),
327/// Err(CacheError::Miss { .. }) => {
328/// // Cache miss - create and cache the item
329/// match create_fn() {
330/// Ok(item) => {
331/// // Attempt to cache the new item
332/// if let Err(cache_err) = cache_item(cache_key, &item).await {
333/// // Log cache failure but return the item anyway
334/// log::warn!("Failed to cache item: {}", cache_err);
335/// }
336/// Ok(item)
337/// }
338/// Err(create_error) => {
339/// Err(CacheError::OperationFailed {
340/// reason: format!("Item creation failed: {}", create_error)
341/// })
342/// }
343/// }
344/// }
345/// Err(CacheError::Expired { key }) => {
346/// // Cache expired - remove and recreate
347/// remove_from_cache(&key).await;
348/// create_fn().map_err(|e| CacheError::OperationFailed {
349/// reason: format!("Recreation after expiry failed: {}", e)
350/// })
351/// }
352/// Err(other) => Err(other),
353/// }
354/// }
355/// ```
356///
357/// ## Cache Health Monitoring
358/// ```no_run
359/// use quetty_server::common::errors::CacheError;
360///
361/// struct CacheMetrics {
362/// hits: u64,
363/// misses: u64,
364/// expirations: u64,
365/// failures: u64,
366/// }
367///
368/// fn update_cache_metrics(error: &CacheError, metrics: &mut CacheMetrics) {
369/// match error {
370/// CacheError::Miss { .. } => {
371/// metrics.misses += 1;
372/// log::debug!("Cache miss recorded");
373/// }
374/// CacheError::Expired { .. } => {
375/// metrics.expirations += 1;
376/// log::debug!("Cache expiration recorded");
377/// }
378/// CacheError::Full { .. } | CacheError::OperationFailed { .. } => {
379/// metrics.failures += 1;
380/// log::warn!("Cache failure recorded: {}", error);
381/// }
382/// }
383/// }
384///
385/// fn calculate_cache_hit_rate(metrics: &CacheMetrics) -> f64 {
386/// let total_requests = metrics.hits + metrics.misses;
387/// if total_requests == 0 {
388/// 0.0
389/// } else {
390/// metrics.hits as f64 / total_requests as f64
391/// }
392/// }
393/// ```
394///
395/// ## Integration with Authentication
396/// ```no_run
397/// use quetty_server::common::errors::CacheError;
398/// use quetty_server::auth::TokenRefreshError;
399///
400/// async fn get_valid_token(user_id: &str) -> Result<String, TokenRefreshError> {
401/// match get_cached_token(user_id).await {
402/// Ok(token) => Ok(token),
403/// Err(CacheError::Miss { .. }) | Err(CacheError::Expired { .. }) => {
404/// // Cache miss or expiry - refresh token
405/// let new_token = refresh_user_token(user_id).await?;
406///
407/// // Attempt to cache the new token
408/// if let Err(cache_err) = cache_token(user_id, &new_token).await {
409/// log::warn!("Failed to cache refreshed token: {}", cache_err);
410/// // Continue anyway - token is still valid
411/// }
412///
413/// Ok(new_token)
414/// }
415/// Err(CacheError::OperationFailed { reason }) => {
416/// // Cache operation failed - try refresh anyway
417/// log::error!("Cache operation failed: {}", reason);
418/// refresh_user_token(user_id).await
419/// }
420/// Err(CacheError::Full { .. }) => {
421/// // Cache full - evict and retry
422/// evict_expired_tokens().await;
423/// match get_cached_token(user_id).await {
424/// Ok(token) => Ok(token),
425/// Err(_) => refresh_user_token(user_id).await,
426/// }
427/// }
428/// }
429/// }
430/// ```
431///
432/// # Cache Strategies
433///
434/// ## Error-Based Cache Management
435/// - **Miss**: Create and cache new data
436/// - **Expired**: Remove expired entry and recreate
437/// - **Full**: Implement LRU or TTL-based eviction
438/// - **Operation Failed**: Fall back to direct data access
439///
440/// ## Performance Considerations
441/// - Cache errors should not block critical operations
442/// - Implement graceful degradation when cache is unavailable
443/// - Monitor cache hit rates and error frequencies
444/// - Use appropriate TTL values to balance freshness and performance
445///
446/// [`Expired`]: CacheError::Expired
447/// [`Miss`]: CacheError::Miss
448/// [`Full`]: CacheError::Full
449/// [`OperationFailed`]: CacheError::OperationFailed
450#[derive(Debug, Error)]
451pub enum CacheError {
452 /// Cache entry has expired and is no longer valid.
453 ///
454 /// This error occurs when attempting to access a cache entry that
455 /// has exceeded its time-to-live (TTL). The entry should be removed
456 /// and recreated if needed.
457 ///
458 /// # Fields
459 /// - `key`: The cache key for the expired entry
460 ///
461 /// # Recovery
462 /// - Remove the expired entry from cache
463 /// - Recreate the data if needed
464 /// - Update cache with fresh data and appropriate TTL
465 #[error("Cache entry expired for key: {key}")]
466 Expired { key: String },
467
468 /// Requested cache entry was not found.
469 ///
470 /// This error occurs when attempting to retrieve a cache entry that
471 /// doesn't exist. This is a normal condition for cold cache scenarios
472 /// or when entries have been evicted.
473 ///
474 /// # Fields
475 /// - `key`: The cache key that was not found
476 ///
477 /// # Recovery
478 /// - Create the data using the original source
479 /// - Cache the newly created data for future requests
480 /// - Consider pre-warming cache for frequently accessed items
481 #[error("Cache miss for key: {key}")]
482 Miss { key: String },
483
484 /// Cache has reached its capacity limit.
485 ///
486 /// This error occurs when attempting to add a new entry to a cache
487 /// that has reached its maximum capacity. This requires cache
488 /// management strategies like eviction.
489 ///
490 /// # Fields
491 /// - `key`: The cache key that couldn't be added
492 ///
493 /// # Recovery
494 /// - Implement cache eviction strategy (LRU, TTL-based, etc.)
495 /// - Remove expired or least recently used entries
496 /// - Consider increasing cache capacity if appropriate
497 /// - Retry the cache operation after eviction
498 #[error("Cache full, unable to add entry for key: {key}")]
499 Full { key: String },
500
501 /// General cache operation failure.
502 ///
503 /// This error represents various cache operation failures that don't
504 /// fit other categories, such as I/O errors, serialization failures,
505 /// or cache system unavailability.
506 ///
507 /// # Fields
508 /// - `reason`: Detailed description of the operation failure
509 ///
510 /// # Recovery
511 /// - Log the detailed error for debugging
512 /// - Fall back to direct data access without caching
513 /// - Consider cache system health checks
514 /// - Implement retry logic for transient failures
515 #[error("Cache operation failed: {reason}")]
516 OperationFailed { reason: String },
517}
518
519/// Helper trait for adding context to errors
520pub trait ErrorContext<T> {
521 /// Add context to an error result
522 fn context(self, msg: &str) -> Result<T, String>;
523
524 /// Add lazy context to an error result
525 fn with_context<F>(self, f: F) -> Result<T, String>
526 where
527 F: FnOnce() -> String;
528}
529
530impl<T, E> ErrorContext<T> for Result<T, E>
531where
532 E: std::fmt::Display,
533{
534 fn context(self, msg: &str) -> Result<T, String> {
535 self.map_err(|e| format!("{msg}: {e}"))
536 }
537
538 fn with_context<F>(self, f: F) -> Result<T, String>
539 where
540 F: FnOnce() -> String,
541 {
542 self.map_err(|e| format!("{}: {e}", f()))
543 }
544}