secret_store_sdk/
errors.rs

1//! Error types and handling for the XJP Secret Store SDK
2//!
3//! This module defines the error types that can be returned by SDK operations.
4//! Errors are designed to provide detailed information for debugging while
5//! maintaining security by not exposing sensitive data.
6//!
7//! # Error Categories
8//!
9//! The SDK uses a structured error system with the following main categories:
10//!
11//! - **HTTP Errors**: API errors with status code, category, and message
12//! - **Network Errors**: Connection and DNS failures
13//! - **Timeout**: Request deadline exceeded
14//! - **Configuration**: Invalid client configuration
15//! - **Deserialization**: Failed to parse API responses
16//!
17//! # Example
18//!
19//! ```no_run
20//! # use secret_store_sdk::{Client, Error};
21//! # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
22//! match client.get_secret("prod", "key", Default::default()).await {
23//!     Ok(secret) => println!("Got secret v{}", secret.version),
24//!     Err(Error::Http { status: 404, .. }) => println!("Secret not found"),
25//!     Err(Error::Http { status: 403, .. }) => println!("Access denied"),
26//!     Err(Error::Timeout) => println!("Request timed out"),
27//!     Err(e) => return Err(e.into()),
28//! }
29//! # Ok(())
30//! # }
31//! ```
32
33use thiserror::Error;
34
35/// Result type alias for the SDK
36pub type Result<T> = std::result::Result<T, Error>;
37
38/// Main error type for the SDK
39#[derive(Error, Debug)]
40pub enum Error {
41    /// HTTP error from the API
42    #[error("http {status}: {category} - {message} (req={request_id:?})")]
43    Http {
44        /// HTTP status code
45        status: u16,
46        /// Error category from server (auth, validation, rate_limit, etc.)
47        category: String,
48        /// Error message from server
49        message: String,
50        /// Request ID from x-request-id header
51        request_id: Option<String>,
52    },
53
54    /// Deserialization error
55    #[error("deserialize: {0}")]
56    Deserialize(String),
57
58    /// Network error
59    #[error("network: {0}")]
60    Network(String),
61
62    /// Request timeout
63    #[error("timeout")]
64    Timeout,
65
66    /// Configuration error
67    #[error("config: {0}")]
68    Config(String),
69
70    /// Other errors
71    #[error("other: {0}")]
72    Other(String),
73}
74
75/// Error categories returned by the server
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ErrorKind {
78    /// Authentication/authorization errors (401/403)
79    Auth,
80    /// Validation errors (400)
81    Validation,
82    /// Resource not found (404)
83    NotFound,
84    /// Rate limit exceeded (429)
85    RateLimit,
86    /// Request timeout (408)
87    Timeout,
88    /// Internal server error (500)
89    Internal,
90    /// Service unavailable (503)
91    ServiceUnavailable,
92    /// Cryptographic operation error
93    Crypto,
94    /// Configuration error
95    Config,
96    /// Other/unknown error
97    Other,
98}
99
100impl ErrorKind {
101    /// Parse error kind from server error category string
102    pub fn from_category(category: &str) -> Self {
103        match category {
104            "auth" => ErrorKind::Auth,
105            "validation" => ErrorKind::Validation,
106            "not_found" => ErrorKind::NotFound,
107            "rate_limit" => ErrorKind::RateLimit,
108            "timeout" => ErrorKind::Timeout,
109            "internal" => ErrorKind::Internal,
110            "service" => ErrorKind::ServiceUnavailable,
111            "crypto" => ErrorKind::Crypto,
112            "config" => ErrorKind::Config,
113            _ => ErrorKind::Other,
114        }
115    }
116}
117
118impl Error {
119    /// Get the error kind for categorization
120    pub fn kind(&self) -> ErrorKind {
121        match self {
122            Error::Http { category, .. } => ErrorKind::from_category(category),
123            Error::Timeout => ErrorKind::Timeout,
124            Error::Config(_) => ErrorKind::Config,
125            _ => ErrorKind::Other,
126        }
127    }
128
129    /// Check if the error is retryable
130    pub fn is_retryable(&self) -> bool {
131        match self {
132            Error::Http { status, .. } => matches!(status, 429 | 500 | 502 | 503 | 504),
133            Error::Network(_) => true,
134            Error::Timeout => true,
135            _ => false,
136        }
137    }
138
139    /// Get the HTTP status code if this is an HTTP error
140    pub fn status_code(&self) -> Option<u16> {
141        match self {
142            Error::Http { status, .. } => Some(*status),
143            _ => None,
144        }
145    }
146
147    /// Get the request ID if available
148    pub fn request_id(&self) -> Option<&str> {
149        match self {
150            Error::Http { request_id, .. } => request_id.as_deref(),
151            _ => None,
152        }
153    }
154
155    /// Create an HTTP error from server response
156    pub(crate) fn from_response(
157        status: u16,
158        error: &str,
159        message: &str,
160        request_id: Option<String>,
161    ) -> Self {
162        Error::Http {
163            status,
164            category: error.to_string(),
165            message: message.to_string(),
166            request_id,
167        }
168    }
169}
170
171/// Server error response structure
172#[derive(Debug, serde::Deserialize)]
173pub(crate) struct ErrorResponse {
174    pub error: String,
175    pub message: String,
176    #[allow(dead_code)]
177    pub timestamp: String,
178    pub status: u16,
179}
180
181impl From<reqwest::Error> for Error {
182    fn from(err: reqwest::Error) -> Self {
183        if err.is_timeout() {
184            Error::Timeout
185        } else if err.is_connect() || err.is_request() {
186            Error::Network(err.to_string())
187        } else if err.is_decode() {
188            Error::Deserialize(err.to_string())
189        } else {
190            Error::Other(err.to_string())
191        }
192    }
193}
194
195impl From<serde_json::Error> for Error {
196    fn from(err: serde_json::Error) -> Self {
197        Error::Deserialize(err.to_string())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_error_kind_from_category() {
207        assert_eq!(ErrorKind::from_category("auth"), ErrorKind::Auth);
208        assert_eq!(
209            ErrorKind::from_category("validation"),
210            ErrorKind::Validation
211        );
212        assert_eq!(ErrorKind::from_category("not_found"), ErrorKind::NotFound);
213        assert_eq!(ErrorKind::from_category("unknown"), ErrorKind::Other);
214    }
215
216    #[test]
217    fn test_error_is_retryable() {
218        let err = Error::Http {
219            status: 429,
220            category: "rate_limit".to_string(),
221            message: "Too many requests".to_string(),
222            request_id: Some("req-123".to_string()),
223        };
224        assert!(err.is_retryable());
225
226        let err = Error::Http {
227            status: 404,
228            category: "not_found".to_string(),
229            message: "Secret not found".to_string(),
230            request_id: None,
231        };
232        assert!(!err.is_retryable());
233
234        let err = Error::Network("Connection failed".to_string());
235        assert!(err.is_retryable());
236
237        let err = Error::Config("Invalid URL".to_string());
238        assert!(!err.is_retryable());
239    }
240
241    #[test]
242    fn test_error_status_code() {
243        let err = Error::Http {
244            status: 401,
245            category: "auth".to_string(),
246            message: "Unauthorized".to_string(),
247            request_id: None,
248        };
249        assert_eq!(err.status_code(), Some(401));
250
251        let err = Error::Timeout;
252        assert_eq!(err.status_code(), None);
253    }
254
255    #[test]
256    fn test_error_request_id() {
257        let err = Error::Http {
258            status: 500,
259            category: "internal".to_string(),
260            message: "Server error".to_string(),
261            request_id: Some("req-456".to_string()),
262        };
263        assert_eq!(err.request_id(), Some("req-456"));
264
265        let err = Error::Network("Failed".to_string());
266        assert_eq!(err.request_id(), None);
267    }
268}