1use std::fmt;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct OssServiceError {
35 pub status_code: u16,
36 pub code: String,
37 pub message: String,
38 pub request_id: String,
39 pub host_id: String,
40 pub resource: Option<String>,
41 pub string_to_sign: Option<String>,
42}
43
44impl OssServiceError {
45 pub fn from_xml(xml: &str) -> Option<Self> {
46 Some(OssServiceError {
47 status_code: 0,
48 code: extract_xml_tag(xml, "Code")?,
49 message: extract_xml_tag(xml, "Message")?,
50 request_id: extract_xml_tag(xml, "RequestId")?,
51 host_id: extract_xml_tag(xml, "HostId")?,
52 resource: extract_xml_tag(xml, "BucketName"),
53 string_to_sign: extract_xml_tag(xml, "StringToSign"),
54 })
55 }
56}
57
58impl fmt::Display for OssServiceError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "{}: {}", self.code, self.message)
61 }
62}
63
64fn extract_xml_tag(xml: &str, tag: &str) -> Option<String> {
65 let open = format!("<{}>", tag);
66 let close = format!("</{}>", tag);
67 let start = xml.find(&open)? + open.len();
68 let end = xml[start..].find(&close)?;
69 Some(xml[start..start + end].trim().to_string())
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum OssErrorKind {
75 ServiceError(Box<OssServiceError>),
76 ConfigError,
77 SigningError,
78 TransportError,
79 IoError,
80 XmlError,
81 CredentialsError,
82 ValidationError,
83 TimeoutError,
84 RetryExhausted,
85 DeserializationError,
86 Unknown,
87}
88
89impl fmt::Display for OssErrorKind {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::ServiceError(e) => write!(f, "[ServiceError] {}", e),
93 Self::ConfigError => write!(f, "[ConfigError] configuration error"),
94 Self::SigningError => write!(f, "[SigningError] signing failed"),
95 Self::TransportError => write!(f, "[TransportError] transport error"),
96 Self::IoError => write!(f, "[IoError] I/O error"),
97 Self::XmlError => write!(f, "[XmlError] XML parsing error"),
98 Self::CredentialsError => write!(f, "[CredentialsError] credentials error"),
99 Self::ValidationError => write!(f, "[ValidationError] validation error"),
100 Self::TimeoutError => write!(f, "[TimeoutError] timeout"),
101 Self::RetryExhausted => write!(f, "[RetryExhausted] retry exhausted"),
102 Self::DeserializationError => write!(f, "[DeserializationError] deserialization error"),
103 Self::Unknown => write!(f, "[Unknown] unknown error"),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Default)]
113pub struct ErrorContext {
114 pub operation: Option<String>,
115 pub bucket: Option<String>,
116 pub object_key: Option<String>,
117 pub request_id: Option<String>,
118 pub endpoint: Option<String>,
119}
120
121impl ErrorContext {
122 fn is_empty(&self) -> bool {
123 self.operation.is_none()
124 && self.bucket.is_none()
125 && self.object_key.is_none()
126 && self.request_id.is_none()
127 && self.endpoint.is_none()
128 }
129}
130
131impl fmt::Display for ErrorContext {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let mut parts = Vec::new();
134 if let Some(op) = &self.operation {
135 parts.push(format!("operation={}", op));
136 }
137 if let Some(b) = &self.bucket {
138 parts.push(format!("bucket={}", b));
139 }
140 if let Some(k) = &self.object_key {
141 parts.push(format!("key={}", k));
142 }
143 if let Some(id) = &self.request_id {
144 parts.push(format!("request_id={}", id));
145 }
146 write!(f, "{}", parts.join(", "))
147 }
148}
149
150#[derive(Debug)]
176pub struct OssError {
177 pub kind: OssErrorKind,
178 pub context: Box<ErrorContext>,
179 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
180}
181
182impl OssError {
183 pub fn service(err: OssServiceError) -> Self {
184 Self {
185 kind: OssErrorKind::ServiceError(Box::new(err)),
186 context: Box::new(ErrorContext::default()),
187 source: None,
188 }
189 }
190
191 pub fn validation(
192 op: impl Into<String>,
193 bucket: impl Into<String>,
194 key: impl Into<String>,
195 ) -> Self {
196 Self {
197 kind: OssErrorKind::ValidationError,
198 context: Box::new(ErrorContext {
199 operation: Some(op.into()),
200 bucket: Some(bucket.into()),
201 object_key: Some(key.into()),
202 request_id: None,
203 endpoint: None,
204 }),
205 source: None,
206 }
207 }
208
209 pub fn transport(source: impl std::error::Error + Send + Sync + 'static) -> Self {
210 Self {
211 kind: OssErrorKind::TransportError,
212 context: Box::new(ErrorContext::default()),
213 source: Some(Box::new(source)),
214 }
215 }
216}
217
218impl fmt::Display for OssError {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 write!(f, "{}", self.kind)?;
221 if !self.context.is_empty() {
222 write!(f, " ({})", self.context)?;
223 }
224 if let Some(ref src) = self.source {
225 write!(f, ": {}", src)?;
226 }
227 Ok(())
228 }
229}
230
231impl std::error::Error for OssError {
232 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
233 self.source
234 .as_ref()
235 .map(|e| e.as_ref() as &dyn std::error::Error)
236 }
237}
238
239pub type Result<T> = std::result::Result<T, OssError>;
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn oss_service_error_from_xml_parses_all_fields() {
250 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
251 <Error>
252 <Code>NoSuchBucket</Code>
253 <Message>The specified bucket does not exist.</Message>
254 <RequestId>5D8A8578E44B3E3FD474D789</RequestId>
255 <HostId>oss-cn-hangzhou.aliyuncs.com</HostId>
256 <BucketName>non-existent-bucket</BucketName>
257 </Error>"#;
258
259 let error = OssServiceError::from_xml(xml).unwrap();
260 assert_eq!(error.code, "NoSuchBucket");
261 assert_eq!(error.message, "The specified bucket does not exist.");
262 assert_eq!(error.request_id, "5D8A8578E44B3E3FD474D789");
263 assert_eq!(error.host_id, "oss-cn-hangzhou.aliyuncs.com");
264 }
265
266 #[test]
267 fn oss_service_error_from_xml_parses_optional_fields() {
268 let xml = r#"<?xml version="1.0"?>
269 <Error>
270 <Code>AccessDenied</Code>
271 <Message>Access Denied</Message>
272 <RequestId>req-id-123</RequestId>
273 <HostId>oss-cn-hangzhou.aliyuncs.com</HostId>
274 <BucketName>my-bucket</BucketName>
275 <StringToSign>PUT\n\n\n...</StringToSign>
276 </Error>"#;
277
278 let error = OssServiceError::from_xml(xml).unwrap();
279 assert_eq!(error.resource.as_deref(), Some("my-bucket"));
280 assert!(error.string_to_sign.is_some());
281 }
282
283 #[test]
284 fn oss_error_display_includes_error_code_and_message() {
285 let err = OssError::service(OssServiceError {
286 status_code: 404,
287 code: "NoSuchBucket".into(),
288 message: "Bucket not found".into(),
289 request_id: "xxx".into(),
290 host_id: "oss-cn-hangzhou.aliyuncs.com".into(),
291 resource: None,
292 string_to_sign: None,
293 });
294 let display = err.to_string();
295 assert!(display.contains("NoSuchBucket"));
296 assert!(display.contains("Bucket not found"));
297 }
298
299 #[test]
300 fn oss_error_kind_validation_error_with_context() {
301 let err = OssError::validation("PutObject", "my-bucket", "my-key");
302 let display = err.to_string();
303 assert!(display.contains("ValidationError"));
304 assert!(display.contains("PutObject"));
305 assert!(display.contains("my-bucket"));
306 assert!(display.contains("my-key"));
307 }
308
309 #[test]
310 fn oss_error_from_http_status_without_body_produces_transport_error() {
311 let err = OssError::transport(std::io::Error::new(
312 std::io::ErrorKind::ConnectionRefused,
313 "connection refused",
314 ));
315 let display = err.to_string();
316 assert!(display.contains("TransportError"));
317 assert!(display.contains("connection refused"));
318 }
319
320 #[test]
321 fn oss_error_is_send_sync() {
322 fn assert_send_sync<T: Send + Sync>() {}
323 assert_send_sync::<OssError>();
324 assert_send_sync::<OssServiceError>();
325 assert_send_sync::<OssErrorKind>();
326 assert_send_sync::<ErrorContext>();
327 }
328
329 #[test]
330 fn oss_error_kind_display_shows_label() {
331 assert!(
332 OssErrorKind::ConfigError
333 .to_string()
334 .contains("ConfigError")
335 );
336 assert!(
337 OssErrorKind::SigningError
338 .to_string()
339 .contains("SigningError")
340 );
341 assert!(
342 OssErrorKind::TimeoutError
343 .to_string()
344 .contains("TimeoutError")
345 );
346 assert!(OssErrorKind::Unknown.to_string().contains("Unknown"));
347 }
348}