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}