Skip to main content

aliyun_oss/
error.rs

1//! Error types for the Alibaba Cloud OSS SDK.
2//!
3//! The primary error type is [`OssError`], which wraps an [`OssErrorKind`]
4//! together with context information and an optional source error.
5//!
6//! # Error handling pattern
7//!
8//! All public functions in this crate return [`Result<T>`](crate::error::Result),
9//! a type alias for `std::result::Result<T, OssError>`.
10//!
11//! ```rust
12//! use aliyun_oss::error::{OssError, OssErrorKind};
13//!
14//! fn handle_error(err: OssError) {
15//!     match err.kind {
16//!         OssErrorKind::ServiceError(ref se) => {
17//!             eprintln!("OSS returned {}: {}", se.status_code, se.message);
18//!         }
19//!         OssErrorKind::ValidationError => {
20//!             eprintln!("Invalid input");
21//!         }
22//!         _ => eprintln!("{}", err),
23//!     }
24//! }
25//! ```
26
27use std::fmt;
28
29/// Represents an OSS service error parsed from an error XML response body.
30///
31/// Contains the OSS-specific error fields returned by the API when a
32/// request fails with a 4xx or 5xx status code.
33#[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/// Categorizes the type of error that occurred.
73#[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/// Contextual information about where an error occurred.
109///
110/// Typically includes the operation name, bucket, and object key
111/// involved when the error was produced.
112#[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/// The unified error type for all OSS SDK operations.
151///
152/// Wraps an [`OssErrorKind`] together with context information and an
153/// optional chained source error. All fallible SDK methods return this
154/// type inside a `Result`.
155///
156/// # Examples
157///
158/// ```rust
159/// use aliyun_oss::error::{OssError, OssErrorKind, OssServiceError};
160///
161/// let err = OssError {
162///     kind: OssErrorKind::ServiceError(Box::new(OssServiceError {
163///         status_code: 404,
164///         code: "NoSuchBucket".into(),
165///         message: "The bucket does not exist.".into(),
166///         request_id: "req-123".into(),
167///         host_id: "oss-cn-hangzhou.aliyuncs.com".into(),
168///         resource: None,
169///         string_to_sign: None,
170///     })),
171///     context: Box::new(Default::default()),
172///     source: None,
173/// };
174/// ```
175#[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
239/// Convenience type alias for `Result<T, OssError>`.
240///
241/// All fallible SDK methods use this type as their return type.
242pub 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}