Skip to main content

azure_storage_blob/models/
error.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4use crate::generated::models::StorageErrorCode;
5use azure_core::{error::ErrorKind, http::RawResponse, xml};
6use serde::Deserialize;
7use serde_json::Value;
8use std::collections::HashMap;
9
10/// A specialized `Result` type for Azure Storage Blob operations.
11pub type Result<T> = std::result::Result<T, StorageError>;
12
13/// Represents an error response from Azure Storage services.
14#[derive(Debug, Clone)]
15pub struct StorageError {
16    /// The HTTP status code.
17    pub status_code: azure_core::http::StatusCode,
18    /// The Storage error code, if available.
19    pub error_code: Option<StorageErrorCode>,
20    /// The error message, if available.
21    pub message: Option<String>,
22    /// The request ID from the x-ms-request-id header, if available.
23    pub request_id: Option<String>,
24    /// A general reason for the error, if available.
25    pub reason: Option<String>,
26    /// Additional authentication error details, if available.
27    pub authentication_error_detail: Option<String>,
28    /// The HTTP status code from the copy source, if available.
29    pub copy_source_status_code: Option<azure_core::http::StatusCode>,
30    /// The error code from the copy source, if available.
31    pub copy_source_error_code: Option<String>,
32    /// The error message from the copy source, if available.
33    pub copy_source_error_message: Option<String>,
34    /// Additional fields from the error response that weren't explicitly mapped.
35    pub additional_error_info: HashMap<String, String>,
36}
37
38impl StorageError {
39    /// Converts a `serde_json::Value` to a String representation, handling nested XML structures.
40    fn value_to_string(value: &Value) -> String {
41        match value {
42            // Primitive types: extract directly
43            Value::Null => "null".to_string(),
44            Value::Bool(b) => b.to_string(),
45            Value::Number(n) => n.to_string(),
46            Value::String(s) => s.clone(),
47
48            // Special case: XML elements with $text field - extract the text content
49            Value::Object(obj) if obj.len() == 1 && obj.contains_key("$text") => obj
50                .get("$text")
51                .and_then(|v| v.as_str())
52                .map(String::from)
53                .unwrap_or_default(),
54
55            // Arrays and complex objects: use serde_json's compact representation
56            _ => serde_json::to_string(value).unwrap_or_default(),
57        }
58    }
59
60    /// Deserializes a `StorageError` from XML body with HTTP response metadata.
61    fn from_xml(
62        status_code: azure_core::http::StatusCode,
63        error_code: Option<StorageErrorCode>,
64        request_id: Option<String>,
65        raw_response: RawResponse,
66    ) -> std::result::Result<Self, azure_core::Error> {
67        #[derive(Deserialize)]
68        #[serde(rename = "Error")]
69        struct StorageErrorXml {
70            #[serde(rename = "Code")]
71            code: Option<String>,
72            #[serde(rename = "Message")]
73            message: Option<String>,
74            #[serde(rename = "Reason")]
75            reason: Option<String>,
76            #[serde(rename = "AuthenticationErrorDetail")]
77            authentication_error_detail: Option<String>,
78            #[serde(rename = "CopySourceStatusCode")]
79            copy_source_status_code: Option<String>,
80            #[serde(rename = "CopySourceErrorCode")]
81            copy_source_error_code: Option<String>,
82            #[serde(rename = "CopySourceErrorMessage")]
83            copy_source_error_message: Option<String>,
84            #[serde(flatten)]
85            additional_fields: HashMap<String, Value>,
86        }
87
88        let xml_fields = xml::from_xml::<_, StorageErrorXml>(raw_response.body())?;
89
90        // Convert additional fields from HashMap<String, Value> to HashMap<String, String>
91        let additional_error_info = xml_fields
92            .additional_fields
93            .iter()
94            .map(|(k, v)| (k.clone(), Self::value_to_string(v)))
95            .collect();
96
97        Ok(StorageError {
98            status_code,
99            error_code,
100            message: xml_fields.message,
101            request_id,
102            reason: xml_fields.reason,
103            authentication_error_detail: xml_fields.authentication_error_detail,
104            copy_source_status_code: xml_fields
105                .copy_source_status_code
106                .and_then(|s| s.parse::<u16>().ok())
107                .map(azure_core::http::StatusCode::from),
108            copy_source_error_code: xml_fields.copy_source_error_code,
109            copy_source_error_message: xml_fields.copy_source_error_message,
110            additional_error_info,
111        })
112    }
113}
114
115impl std::fmt::Display for StorageError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        writeln!(f, "HTTP Status Code: {}", self.status_code)?;
118
119        if let Some(request_id) = &self.request_id {
120            writeln!(f, "Request ID: {}", request_id)?;
121        }
122
123        if let Some(error_code) = &self.error_code {
124            writeln!(f, "Storage Error Code: {}", error_code)?;
125        }
126
127        if let Some(message) = &self.message {
128            writeln!(f, "Error Message: {}", message)?;
129        }
130
131        if let Some(reason) = &self.reason {
132            writeln!(f, "Reason: {}", reason)?;
133        }
134
135        if let Some(detail) = &self.authentication_error_detail {
136            writeln!(f, "Authentication Error Detail: {}", detail)?;
137        }
138
139        if let Some(status) = &self.copy_source_status_code {
140            writeln!(f, "Copy Source Status Code: {}", status)?;
141        }
142
143        if let Some(code) = &self.copy_source_error_code {
144            writeln!(f, "Copy Source Error Code: {}", code)?;
145        }
146
147        if let Some(message) = &self.copy_source_error_message {
148            writeln!(f, "Copy Source Error Message: {}", message)?;
149        }
150
151        if !self.additional_error_info.is_empty() {
152            writeln!(f, "\nAdditional Error Info:")?;
153            for (key, value) in &self.additional_error_info {
154                writeln!(f, "{}: {}", key, value)?;
155            }
156        }
157
158        Ok(())
159    }
160}
161
162impl std::error::Error for StorageError {}
163
164impl TryFrom<azure_core::Error> for StorageError {
165    type Error = azure_core::Error;
166
167    fn try_from(error: azure_core::Error) -> std::result::Result<Self, Self::Error> {
168        match error.kind() {
169            ErrorKind::HttpResponse {
170                status,
171                raw_response: Some(raw_response),
172                ..
173            } => {
174                let headers = raw_response.headers();
175                let body = raw_response.body();
176
177                let error_code = headers
178                    .get_optional_string(&azure_core::http::headers::HeaderName::from_static(
179                        "x-ms-error-code",
180                    ))
181                    .and_then(|code| {
182                        code.parse()
183                            .ok()
184                            .or(Some(StorageErrorCode::UnknownValue(code)))
185                    });
186
187                let request_id = headers.get_optional_string(
188                    &azure_core::http::headers::HeaderName::from_static("x-ms-request-id"),
189                );
190
191                if body.is_empty() {
192                    // For bodiless responses, use the canonical reason phrase as a fallback message
193                    let message = Some(status.canonical_reason().to_string());
194
195                    return Ok(StorageError {
196                        status_code: *status,
197                        error_code,
198                        message,
199                        request_id,
200                        reason: None,
201                        authentication_error_detail: None,
202                        copy_source_status_code: None,
203                        copy_source_error_code: None,
204                        copy_source_error_message: None,
205                        additional_error_info: HashMap::new(),
206                    });
207                }
208
209                StorageError::from_xml(
210                    *status,
211                    error_code,
212                    request_id,
213                    raw_response.as_ref().clone(),
214                )
215            }
216            // Return a minimal StorageError if raw_response is missing
217            ErrorKind::HttpResponse {
218                status,
219                raw_response: None,
220                ..
221            } => {
222                let message = Some(status.canonical_reason().to_string());
223                Ok(StorageError {
224                    status_code: *status,
225                    error_code: None,
226                    message,
227                    request_id: None,
228                    reason: None,
229                    authentication_error_detail: None,
230                    copy_source_status_code: None,
231                    copy_source_error_code: None,
232                    copy_source_error_message: None,
233                    additional_error_info: HashMap::new(),
234                })
235            }
236            _ => Err(azure_core::Error::with_message(
237                azure_core::error::ErrorKind::DataConversion,
238                "ErrorKind was not HttpResponse and could not be parsed.",
239            )),
240        }
241    }
242}