Skip to main content

redis_cloud/
error.rs

1//! Error types for Redis Cloud API client
2//!
3//! This module provides error handling for all API operations, including
4//! typed errors for common HTTP status codes and network failures.
5//!
6//! # Error Types
7//!
8//! - `CloudError::BadRequest` - HTTP 400 errors
9//! - `CloudError::AuthenticationFailed` - HTTP 401 errors
10//! - `CloudError::Forbidden` - HTTP 403 errors
11//! - `CloudError::NotFound` - HTTP 404 errors
12//! - `CloudError::RateLimited` - HTTP 429 errors
13//! - `CloudError::InternalServerError` - HTTP 500 errors
14//! - `CloudError::ServiceUnavailable` - HTTP 503 errors
15//!
16//! # Retryable Errors
17//!
18//! Some errors are considered retryable (transient failures that may succeed on retry):
19//! - Rate limited (429)
20//! - Service unavailable (503)
21//! - Connection/request errors (network issues)
22//!
23//! Use `CloudError::is_retryable()` to check if an error should be retried.
24
25use thiserror::Error;
26
27/// Errors that can occur when interacting with the Redis Cloud API
28#[derive(Error, Debug, Clone)]
29pub enum CloudError {
30    /// HTTP request failed (network error, timeout, etc.)
31    #[error("HTTP request failed: {0}")]
32    Request(String),
33
34    /// Bad Request (400) - Invalid request parameters
35    #[error("Bad Request (400): {message}")]
36    BadRequest {
37        /// Error message from the API
38        message: String,
39    },
40
41    /// Authentication failed (401) - Invalid or missing credentials
42    #[error("Authentication failed (401): {message}")]
43    AuthenticationFailed {
44        /// Error message from the API
45        message: String,
46    },
47
48    /// Forbidden (403) - Insufficient permissions
49    #[error("Forbidden (403): {message}")]
50    Forbidden {
51        /// Error message from the API
52        message: String,
53    },
54
55    /// Not Found (404) - Resource does not exist
56    #[error("Not Found (404): {message}")]
57    NotFound {
58        /// Error message from the API
59        message: String,
60    },
61
62    /// Precondition Failed (412) - Feature flag is disabled
63    #[error("Precondition Failed (412): Feature flag for this flow is off")]
64    PreconditionFailed,
65
66    /// Rate Limited (429) - Too many requests
67    #[error("Rate Limited (429): {message}")]
68    RateLimited {
69        /// Error message from the API
70        message: String,
71    },
72
73    /// Internal Server Error (500) - Server-side error
74    #[error("Internal Server Error (500): {message}")]
75    InternalServerError {
76        /// Error message from the API
77        message: String,
78    },
79
80    /// Service Unavailable (503) - Server temporarily unavailable
81    #[error("Service Unavailable (503): {message}")]
82    ServiceUnavailable {
83        /// Error message from the API
84        message: String,
85    },
86
87    /// Generic API error for other HTTP status codes
88    #[error("API error ({code}): {message}")]
89    ApiError {
90        /// HTTP status code
91        code: u16,
92        /// Error message from the API
93        message: String,
94    },
95
96    /// Connection error (failed to establish connection)
97    #[error("Connection error: {0}")]
98    ConnectionError(String),
99
100    /// JSON serialization/deserialization error
101    #[error("JSON error: {0}")]
102    JsonError(String),
103}
104
105impl CloudError {
106    /// Returns true if this error is retryable.
107    ///
108    /// Retryable errors include:
109    /// - Rate limited (429)
110    /// - Service unavailable (503)
111    /// - Connection/request errors (may be transient network issues)
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use redis_cloud::CloudError;
117    ///
118    /// let error = CloudError::RateLimited { message: "Too many requests".to_string() };
119    /// assert!(error.is_retryable());
120    ///
121    /// let error = CloudError::NotFound { message: "Resource not found".to_string() };
122    /// assert!(!error.is_retryable());
123    /// ```
124    #[must_use]
125    pub fn is_retryable(&self) -> bool {
126        matches!(
127            self,
128            CloudError::RateLimited { .. }
129                | CloudError::ServiceUnavailable { .. }
130                | CloudError::Request(_)
131                | CloudError::ConnectionError(_)
132        )
133    }
134
135    /// Returns true if this error indicates a resource was not found.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use redis_cloud::CloudError;
141    ///
142    /// let error = CloudError::NotFound { message: "Database not found".to_string() };
143    /// assert!(error.is_not_found());
144    ///
145    /// let error = CloudError::BadRequest { message: "Invalid request".to_string() };
146    /// assert!(!error.is_not_found());
147    /// ```
148    #[must_use]
149    pub fn is_not_found(&self) -> bool {
150        matches!(self, CloudError::NotFound { .. })
151    }
152
153    /// Returns true if this error indicates an authentication or authorization failure.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use redis_cloud::CloudError;
159    ///
160    /// let error = CloudError::AuthenticationFailed { message: "Invalid credentials".to_string() };
161    /// assert!(error.is_unauthorized());
162    ///
163    /// let error = CloudError::Forbidden { message: "Access denied".to_string() };
164    /// assert!(error.is_unauthorized());
165    /// ```
166    #[must_use]
167    pub fn is_unauthorized(&self) -> bool {
168        matches!(
169            self,
170            CloudError::AuthenticationFailed { .. } | CloudError::Forbidden { .. }
171        )
172    }
173
174    /// Returns true if this error indicates a server-side error.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use redis_cloud::CloudError;
180    ///
181    /// let error = CloudError::InternalServerError { message: "Server error".to_string() };
182    /// assert!(error.is_server_error());
183    ///
184    /// let error = CloudError::ServiceUnavailable { message: "Service down".to_string() };
185    /// assert!(error.is_server_error());
186    /// ```
187    #[must_use]
188    pub fn is_server_error(&self) -> bool {
189        matches!(
190            self,
191            CloudError::InternalServerError { .. } | CloudError::ServiceUnavailable { .. }
192        )
193    }
194
195    /// Returns true if this error indicates a timeout.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use redis_cloud::CloudError;
201    ///
202    /// let error = CloudError::ConnectionError("connection timeout".to_string());
203    /// assert!(error.is_timeout());
204    ///
205    /// let error = CloudError::Request("request timeout occurred".to_string());
206    /// assert!(error.is_timeout());
207    ///
208    /// let error = CloudError::NotFound { message: "Not found".to_string() };
209    /// assert!(!error.is_timeout());
210    /// ```
211    #[must_use]
212    pub fn is_timeout(&self) -> bool {
213        match self {
214            CloudError::ConnectionError(msg) | CloudError::Request(msg) => {
215                msg.to_lowercase().contains("timeout")
216            }
217            _ => false,
218        }
219    }
220
221    /// Returns true if this error indicates rate limiting.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use redis_cloud::CloudError;
227    ///
228    /// let error = CloudError::RateLimited { message: "Too many requests".to_string() };
229    /// assert!(error.is_rate_limited());
230    ///
231    /// let error = CloudError::BadRequest { message: "Invalid request".to_string() };
232    /// assert!(!error.is_rate_limited());
233    /// ```
234    #[must_use]
235    pub fn is_rate_limited(&self) -> bool {
236        matches!(self, CloudError::RateLimited { .. })
237    }
238
239    /// Returns true if this error indicates a conflict (precondition failed).
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use redis_cloud::CloudError;
245    ///
246    /// let error = CloudError::PreconditionFailed;
247    /// assert!(error.is_conflict());
248    ///
249    /// let error = CloudError::BadRequest { message: "Invalid request".to_string() };
250    /// assert!(!error.is_conflict());
251    /// ```
252    #[must_use]
253    pub fn is_conflict(&self) -> bool {
254        matches!(self, CloudError::PreconditionFailed)
255    }
256
257    /// Returns true if this error indicates a bad request.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use redis_cloud::CloudError;
263    ///
264    /// let error = CloudError::BadRequest { message: "Invalid parameters".to_string() };
265    /// assert!(error.is_bad_request());
266    ///
267    /// let error = CloudError::NotFound { message: "Not found".to_string() };
268    /// assert!(!error.is_bad_request());
269    /// ```
270    #[must_use]
271    pub fn is_bad_request(&self) -> bool {
272        matches!(self, CloudError::BadRequest { .. })
273    }
274}
275
276impl From<reqwest::Error> for CloudError {
277    fn from(err: reqwest::Error) -> Self {
278        CloudError::Request(err.to_string())
279    }
280}
281
282impl From<serde_json::Error> for CloudError {
283    fn from(err: serde_json::Error) -> Self {
284        CloudError::JsonError(err.to_string())
285    }
286}
287
288/// Result type alias for Redis Cloud operations
289pub type Result<T> = std::result::Result<T, CloudError>;