reqsign_core/
error.rs

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