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>;