1use thiserror::Error;
2
3pub(crate) const MAX_ERROR_BODY_CHARS: usize = 200;
5
6#[derive(Debug, Error)]
8pub enum StsError {
9 #[error("HTTP request failed: {0}")]
11 HttpClient(#[from] reqwest::Error),
12
13 #[error("HTTP error: {0}")]
15 Http(String),
16
17 #[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 #[error("signature error: {0}")]
28 Signature(String),
29
30 #[error("credential error: {0}")]
32 Credential(String),
33
34 #[error("deserialization error: {0}")]
36 Deserialize(#[from] serde_json::Error),
37
38 #[error("config error: {0}")]
40 Config(String),
41
42 #[error("validation error: {0}")]
44 Validation(String),
45}
46
47impl StsError {
48 pub fn is_retryable(&self) -> bool {
59 match self {
60 StsError::HttpClient(e) => e.is_timeout() || e.is_connect(),
62 StsError::Http(_) => true,
63
64 StsError::Api { code, .. } => {
66 if code == "Throttling" || code == "ServiceUnavailable" {
68 return true;
69 }
70 code.starts_with("Internal") || code.starts_with("Service")
72 }
73
74 StsError::Signature(_)
76 | StsError::Credential(_)
77 | StsError::Deserialize(_)
78 | StsError::Config(_)
79 | StsError::Validation(_) => false,
80 }
81 }
82
83 pub fn request_id(&self) -> Option<&str> {
85 match self {
86 StsError::Api { request_id, .. } => Some(request_id),
87 _ => None,
88 }
89 }
90
91 pub fn error_code(&self) -> Option<&str> {
93 match self {
94 StsError::Api { code, .. } => Some(code),
95 _ => None,
96 }
97 }
98}
99
100pub type Result<T> = std::result::Result<T, StsError>;
102
103pub(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 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}