reqsign_core/
error.rs

1use std::fmt;
2use thiserror::Error;
3
4/// The error type for reqsign operations
5#[derive(Error)]
6#[error("{message}")]
7pub struct Error {
8    /// The category of error that occurred
9    kind: ErrorKind,
10
11    /// Human-readable error message
12    message: String,
13
14    /// The underlying error source
15    #[source]
16    source: Option<anyhow::Error>,
17
18    /// Additional context information for debugging
19    context: Vec<String>,
20
21    /// Whether this error is retryable
22    retryable: bool,
23}
24
25/// The kind of error that occurred
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ErrorKind {
28    /// Credentials are invalid, expired, or malformed
29    /// User action: Check credential format, refresh if expired
30    CredentialInvalid,
31
32    /// Permission denied when accessing credentials or resources
33    /// User action: Check IAM policies, role trust relationships
34    PermissionDenied,
35
36    /// Required configuration is missing or invalid
37    /// User action: Check configuration files, environment variables
38    ConfigInvalid,
39
40    /// Request cannot be signed or is malformed
41    /// User action: Check request parameters, headers
42    RequestInvalid,
43
44    /// Rate limit exceeded
45    /// User action: Implement backoff, check quotas
46    RateLimited,
47
48    /// Unexpected error that doesn't fit other categories
49    /// User action: Check logs, report bug if persistent
50    Unexpected,
51}
52
53impl Error {
54    /// Create a new error with the given kind and message
55    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
56        Self {
57            kind,
58            message: message.into(),
59            source: None,
60            context: Vec::new(),
61            retryable: kind.default_retryable(),
62        }
63    }
64
65    /// Add a source error
66    pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
67        self.source = Some(source.into());
68        self
69    }
70
71    /// Add context information for debugging
72    pub fn with_context(mut self, context: impl fmt::Display) -> Self {
73        self.context.push(context.to_string());
74        self
75    }
76
77    /// Override the retryable status
78    pub fn set_retryable(mut self, retryable: bool) -> Self {
79        self.retryable = retryable;
80        self
81    }
82
83    /// Get the error kind
84    pub fn kind(&self) -> ErrorKind {
85        self.kind
86    }
87
88    /// Check if this error is retryable
89    pub fn is_retryable(&self) -> bool {
90        self.retryable
91    }
92
93    /// Get the context information
94    pub fn context(&self) -> &[String] {
95        &self.context
96    }
97}
98
99impl ErrorKind {
100    /// Default retryable status for each error kind
101    fn default_retryable(&self) -> bool {
102        matches!(self, ErrorKind::RateLimited)
103    }
104}
105
106// Convenience constructors
107impl Error {
108    /// Create a credential invalid error
109    pub fn credential_invalid(message: impl Into<String>) -> Self {
110        Self::new(ErrorKind::CredentialInvalid, message)
111    }
112
113    /// Create a permission denied error
114    pub fn permission_denied(message: impl Into<String>) -> Self {
115        Self::new(ErrorKind::PermissionDenied, message)
116    }
117
118    /// Create a config invalid error
119    pub fn config_invalid(message: impl Into<String>) -> Self {
120        Self::new(ErrorKind::ConfigInvalid, message)
121    }
122
123    /// Create a request invalid error
124    pub fn request_invalid(message: impl Into<String>) -> Self {
125        Self::new(ErrorKind::RequestInvalid, message)
126    }
127
128    /// Create a rate limited error
129    pub fn rate_limited(message: impl Into<String>) -> Self {
130        Self::new(ErrorKind::RateLimited, message)
131    }
132
133    /// Create an unexpected error
134    pub fn unexpected(message: impl Into<String>) -> Self {
135        Self::new(ErrorKind::Unexpected, message)
136    }
137}
138
139// Custom Debug implementation for better error display
140impl fmt::Debug for Error {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        let mut debug = f.debug_struct("Error");
143        debug.field("kind", &self.kind);
144        debug.field("message", &self.message);
145
146        if !self.context.is_empty() {
147            debug.field("context", &self.context);
148        }
149
150        if let Some(source) = &self.source {
151            debug.field("source", source);
152        }
153
154        debug.field("retryable", &self.retryable);
155        debug.finish()
156    }
157}
158
159impl fmt::Display for ErrorKind {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            ErrorKind::CredentialInvalid => write!(f, "invalid credentials"),
163            ErrorKind::PermissionDenied => write!(f, "permission denied"),
164            ErrorKind::ConfigInvalid => write!(f, "invalid configuration"),
165            ErrorKind::RequestInvalid => write!(f, "invalid request"),
166            ErrorKind::RateLimited => write!(f, "rate limited"),
167            ErrorKind::Unexpected => write!(f, "unexpected error"),
168        }
169    }
170}
171
172/// Convenience type alias for Results
173pub type Result<T> = std::result::Result<T, Error>;
174
175// Common From implementations for better ergonomics
176impl From<anyhow::Error> for Error {
177    fn from(err: anyhow::Error) -> Self {
178        Self::unexpected(err.to_string()).with_source(err)
179    }
180}
181
182impl From<std::fmt::Error> for Error {
183    fn from(err: std::fmt::Error) -> Self {
184        Self::unexpected(err.to_string()).with_source(err)
185    }
186}
187
188impl From<http::Error> for Error {
189    fn from(err: http::Error) -> Self {
190        Self::request_invalid(err.to_string()).with_source(err)
191    }
192}
193
194impl From<http::header::InvalidHeaderValue> for Error {
195    fn from(err: http::header::InvalidHeaderValue) -> Self {
196        Self::request_invalid(err.to_string()).with_source(err)
197    }
198}
199
200impl From<http::uri::InvalidUri> for Error {
201    fn from(err: http::uri::InvalidUri) -> Self {
202        Self::request_invalid(err.to_string()).with_source(err)
203    }
204}
205
206impl From<http::uri::InvalidUriParts> for Error {
207    fn from(err: http::uri::InvalidUriParts) -> Self {
208        Self::request_invalid(err.to_string()).with_source(err)
209    }
210}
211
212impl From<std::string::FromUtf8Error> for Error {
213    fn from(err: std::string::FromUtf8Error) -> Self {
214        Self::unexpected(err.to_string()).with_source(err)
215    }
216}
217
218impl From<std::io::Error> for Error {
219    fn from(err: std::io::Error) -> Self {
220        use std::io::ErrorKind;
221
222        let kind = err.kind();
223        let message = err.to_string();
224        let source = anyhow::Error::from(err);
225
226        match kind {
227            ErrorKind::NotFound => Self::config_invalid(message).with_source(source),
228            ErrorKind::PermissionDenied => Self::permission_denied(message).with_source(source),
229            _ => Self::unexpected(message)
230                .with_source(source)
231                .set_retryable(matches!(
232                    kind,
233                    ErrorKind::TimedOut | ErrorKind::Interrupted | ErrorKind::ConnectionRefused
234                )),
235        }
236    }
237}
238
239impl From<http::header::InvalidHeaderName> for Error {
240    fn from(err: http::header::InvalidHeaderName) -> Self {
241        Self::request_invalid(err.to_string()).with_source(err)
242    }
243}
244
245impl From<http::header::ToStrError> for Error {
246    fn from(err: http::header::ToStrError) -> Self {
247        Self::request_invalid(err.to_string()).with_source(err)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_error_creation() {
257        let err = Error::credential_invalid("token expired");
258        assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
259        assert!(!err.is_retryable());
260    }
261
262    #[test]
263    fn test_error_with_context() {
264        let err = Error::permission_denied("access denied")
265            .with_context("role: arn:aws:iam::123456789012:role/MyRole")
266            .with_context("operation: AssumeRole");
267
268        assert_eq!(err.context().len(), 2);
269        assert_eq!(
270            err.context()[0],
271            "role: arn:aws:iam::123456789012:role/MyRole"
272        );
273        assert_eq!(err.context()[1], "operation: AssumeRole");
274    }
275
276    #[test]
277    fn test_rate_limited_default_retryable() {
278        let err = Error::rate_limited("too many requests");
279        assert!(err.is_retryable());
280    }
281
282    #[test]
283    fn test_override_retryable() {
284        let err = Error::unexpected("network timeout").set_retryable(true);
285        assert!(err.is_retryable());
286
287        let err = Error::rate_limited("quota exceeded").set_retryable(false);
288        assert!(!err.is_retryable());
289    }
290
291    #[test]
292    fn test_error_debug_format() {
293        let err = Error::config_invalid("missing region")
294            .with_context("file: ~/.aws/config")
295            .with_context("profile: default");
296
297        let debug_str = format!("{:?}", err);
298        assert!(debug_str.contains("ConfigInvalid"));
299        assert!(debug_str.contains("missing region"));
300        assert!(debug_str.contains("~/.aws/config"));
301    }
302}