Skip to main content

rs_ali_sts/
error.rs

1use thiserror::Error;
2
3/// Maximum characters to include in error message body for debugging.
4pub(crate) const MAX_ERROR_BODY_CHARS: usize = 200;
5
6/// Errors that can occur when using the STS SDK.
7#[derive(Debug, Error)]
8pub enum StsError {
9    /// HTTP/network layer error from reqwest.
10    #[error("HTTP request failed: {0}")]
11    HttpClient(#[from] reqwest::Error),
12
13    /// Unexpected HTTP response (non-JSON error body).
14    #[error("HTTP error: {0}")]
15    Http(String),
16
17    /// Alibaba Cloud API returned a business error.
18    #[error("API error (RequestId: {request_id}): [{code}] {message}")]
19    Api {
20        request_id: String,
21        code: String,
22        message: String,
23        recommend: Option<String>,
24    },
25
26    /// Signature computation error.
27    #[error("signature error: {0}")]
28    Signature(String),
29
30    /// Credential not found or invalid.
31    #[error("credential error: {0}")]
32    Credential(String),
33
34    /// Response deserialization error.
35    #[error("deserialization error: {0}")]
36    Deserialize(#[from] serde_json::Error),
37
38    /// Config file parse error.
39    #[error("config error: {0}")]
40    Config(String),
41
42    /// Validation error for request parameters.
43    #[error("validation error: {0}")]
44    Validation(String),
45}
46
47impl StsError {
48    /// Returns `true` if the error is potentially recoverable by retrying.
49    ///
50    /// Retryable errors include:
51    /// - Network/HTTP errors (timeouts, connection issues)
52    /// - Server errors (5xx)
53    ///
54    /// Non-retryable errors include:
55    /// - Authentication/credential errors
56    /// - Validation errors
57    /// - Client errors (4xx except 429)
58    pub fn is_retryable(&self) -> bool {
59        match self {
60            // Network errors are generally retryable
61            StsError::HttpClient(e) => e.is_timeout() || e.is_connect(),
62            StsError::Http(_) => true,
63
64            // API errors: check the code
65            StsError::Api { code, .. } => {
66                // Rate limiting is retryable
67                if code == "Throttling" || code == "ServiceUnavailable" {
68                    return true;
69                }
70                // Server errors (5xx-like) are retryable
71                code.starts_with("Internal") || code.starts_with("Service")
72            }
73
74            // These are never retryable
75            StsError::Signature(_)
76            | StsError::Credential(_)
77            | StsError::Deserialize(_)
78            | StsError::Config(_)
79            | StsError::Validation(_) => false,
80        }
81    }
82
83    /// Returns the request ID if this is an API error.
84    pub fn request_id(&self) -> Option<&str> {
85        match self {
86            StsError::Api { request_id, .. } => Some(request_id),
87            _ => None,
88        }
89    }
90
91    /// Returns the error code if this is an API error.
92    pub fn error_code(&self) -> Option<&str> {
93        match self {
94            StsError::Api { code, .. } => Some(code),
95            _ => None,
96        }
97    }
98}
99
100/// A specialized Result type for STS operations.
101pub type Result<T> = std::result::Result<T, StsError>;
102
103/// Truncates a string to at most `max_chars` characters on a valid UTF-8 boundary.
104pub(crate) fn truncate_str(s: &str, max_chars: usize) -> &str {
105    match s.char_indices().nth(max_chars) {
106        Some((idx, _)) => &s[..idx],
107        None => s,
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn api_error_display() {
117        let err = StsError::Api {
118            request_id: "req-123".to_string(),
119            code: "InvalidParameter".to_string(),
120            message: "The specified RoleArn is invalid.".to_string(),
121            recommend: None,
122        };
123        let msg = err.to_string();
124        assert!(msg.contains("req-123"));
125        assert!(msg.contains("InvalidParameter"));
126        assert!(msg.contains("The specified RoleArn is invalid."));
127    }
128
129    #[test]
130    fn http_error_display() {
131        let err = StsError::Http("HTTP 502 with body: Bad Gateway".to_string());
132        assert_eq!(
133            err.to_string(),
134            "HTTP error: HTTP 502 with body: Bad Gateway"
135        );
136    }
137
138    #[test]
139    fn credential_error_display() {
140        let err = StsError::Credential("no credential found".to_string());
141        assert_eq!(err.to_string(), "credential error: no credential found");
142    }
143
144    #[test]
145    fn signature_error_display() {
146        let err = StsError::Signature("HMAC computation failed".to_string());
147        assert_eq!(err.to_string(), "signature error: HMAC computation failed");
148    }
149
150    #[test]
151    fn config_error_display() {
152        let err = StsError::Config("invalid INI format".to_string());
153        assert_eq!(err.to_string(), "config error: invalid INI format");
154    }
155
156    #[test]
157    fn truncate_str_short() {
158        assert_eq!(truncate_str("hello", 10), "hello");
159    }
160
161    #[test]
162    fn truncate_str_exact() {
163        assert_eq!(truncate_str("hello", 5), "hello");
164    }
165
166    #[test]
167    fn truncate_str_long() {
168        assert_eq!(truncate_str("hello world", 5), "hello");
169    }
170
171    #[test]
172    fn truncate_str_multibyte() {
173        // "中文测试" is 4 characters, each 3 bytes in UTF-8
174        let s = "中文测试数据";
175        assert_eq!(truncate_str(s, 4), "中文测试");
176    }
177
178    #[test]
179    fn truncate_str_empty() {
180        assert_eq!(truncate_str("", 10), "");
181    }
182}