From 9a785c5305e5c44b338cfb161a3911c2d224531c Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Tue, 16 Sep 2025 14:43:27 -0700 Subject: [PATCH 1/5] Testcase for error handling --- .../azure_storage_blob/tests/blob_client.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index 54ece2643d..995f19d9e9 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use azure_core::{ + error::ErrorKind, http::{RequestContent, StatusCode}, Bytes, }; @@ -16,6 +17,7 @@ use azure_storage_blob::models::{ }; use azure_storage_blob_test::{create_test_blob, get_blob_name, get_container_client}; +use core::error; use std::{collections::HashMap, error::Error, time::Duration}; use tokio::time; @@ -477,3 +479,41 @@ async fn test_get_account_info(ctx: TestContext) -> Result<(), Box> { Ok(()) } + +#[recorded::test] +async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + println!("Top of Test"); + let recording = ctx.recording(); + let container_client = get_container_client(recording, true).await?; + let blob_client = container_client.blob_client(get_blob_name(recording)); + + // Act + let response = blob_client.get_properties(None).await; + let error_response = response.unwrap_err(); + println!("Error debug print: {:?}", error_response); + + let error_kind = error_response.kind(); + + println!("Before Match"); + // Match out of the error_kind struct + if let ErrorKind::HttpResponse { + raw_response: Some(inner_raw_response), + .. + } = error_kind + { + println!("Inside of Match"); + let status = inner_raw_response.status(); + let body = inner_raw_response.body(); // Expected to be empty + let headers = inner_raw_response.headers(); + + println!("Status Code: {}", status); + for (key, value) in headers.iter() { + println!("Header: {} = {}", key.as_str(), value.as_str()); + } + println!("Body: {:?}", body); + } + + println!("Bottom of Test"); + Ok(()) +} From d7f563d433b0474ec4e67b61a775ac4b05a2320c Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Mon, 22 Sep 2025 14:31:28 -0700 Subject: [PATCH 2/5] Still can't access raw_response --- .../azure_storage_blob/tests/blob_client.rs | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index efc6d06eb7..596a2863d3 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -486,37 +486,29 @@ async fn test_get_account_info(ctx: TestContext) -> Result<(), Box> { #[recorded::test] async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box> { // Recording Setup - println!("Top of Test"); let recording = ctx.recording(); let container_client = get_container_client(recording, true).await?; let blob_client = container_client.blob_client(get_blob_name(recording)); // Act - let response = blob_client.get_properties(None).await; + let response = blob_client.download(None).await; let error_response = response.unwrap_err(); - println!("Error debug print: {:?}", error_response); - let error_kind = error_response.kind(); + assert!(matches!(error_kind, ErrorKind::HttpResponse { .. })); - println!("Before Match"); + println!("**[BEFORE Matching ErrorKind Type]**"); // Match out of the error_kind struct if let ErrorKind::HttpResponse { - raw_response: Some(inner_raw_response), + status, + error_code, + raw_response, .. } = error_kind { - println!("Inside of Match"); - let status = inner_raw_response.status(); - let body = inner_raw_response.body(); // Expected to be empty - let headers = inner_raw_response.headers(); - - println!("Status Code: {}", status); - for (key, value) in headers.iter() { - println!("Header: {} = {}", key.as_str(), value.as_str()); - } - println!("Body: {:?}", body); + println!("Status code:{}", status); + println!("Error code:{}", error_code.clone().unwrap()); + println!("Raw response:{:?}", raw_response.clone().unwrap()); } - - println!("Bottom of Test"); + println!("**[AFTER Matching ErrorKind Type]**"); Ok(()) } From e026aae9dd22fb670d66dd21387fe026cf5d4dc6 Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Thu, 25 Sep 2025 17:30:44 -0700 Subject: [PATCH 3/5] First draft of StorageError model type --- .../src/models/extensions.rs | 62 ++++++++++++++++++- .../azure_storage_blob/src/models/mod.rs | 36 +++++++++++ .../azure_storage_blob/tests/blob_client.rs | 20 ++---- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/sdk/storage/azure_storage_blob/src/models/extensions.rs b/sdk/storage/azure_storage_blob/src/models/extensions.rs index 03246605a8..f34798ee7b 100644 --- a/sdk/storage/azure_storage_blob/src/models/extensions.rs +++ b/sdk/storage/azure_storage_blob/src/models/extensions.rs @@ -3,9 +3,9 @@ use crate::models::{ AppendBlobClientCreateOptions, BlobTag, BlobTags, BlockBlobClientUploadBlobFromUrlOptions, - BlockBlobClientUploadOptions, PageBlobClientCreateOptions, + BlockBlobClientUploadOptions, PageBlobClientCreateOptions, StorageError, StorageErrorCode, }; -use azure_core::error::ErrorKind; +use azure_core::{error::ErrorKind, http::headers::Headers}; use std::collections::HashMap; /// Augments the current options bag to only create if the Page blob does not already exist. @@ -109,3 +109,61 @@ impl From> for BlobTags { } } } + +impl TryFrom for StorageError { + type Error = azure_core::Error; + + fn try_from(error: azure_core::Error) -> Result { + let message = error.to_string(); + + match error.kind() { + ErrorKind::HttpResponse { + status, + error_code, + raw_response, + } => { + let error_code = error_code.as_ref().ok_or_else(|| { + azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "error_code field missing from HttpResponse.", + ) + })?; + + let headers = raw_response + .as_ref() + .map(|raw_resp| raw_resp.headers().clone()) + .unwrap_or_default(); + + let error_code_enum = error_code + .parse() + .unwrap_or(StorageErrorCode::UnknownValue(error_code.clone())); + + Ok(StorageError { + status_code: *status, + error_code: error_code_enum, + message, + headers, + }) + } + _ => Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "ErrorKind was not HttpResponse and could not be parsed.", + )), + } + } +} + +impl std::fmt::Display for StorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Standard Error Message: {}\n", self.message)?; + writeln!(f, "Http Status Code: {}", self.status_code)?; + writeln!(f, "Storage Error Code: {}", self.error_code)?; + writeln!(f, "Response Headers:")?; + + for (name, value) in self.headers.iter() { + writeln!(f, " \"{}\": \"{}\"", name.as_str(), value.as_str())?; + } + + Ok(()) + } +} diff --git a/sdk/storage/azure_storage_blob/src/models/mod.rs b/sdk/storage/azure_storage_blob/src/models/mod.rs index 25f0e07366..7696c3718c 100644 --- a/sdk/storage/azure_storage_blob/src/models/mod.rs +++ b/sdk/storage/azure_storage_blob/src/models/mod.rs @@ -83,3 +83,39 @@ pub use crate::generated::models::{ VecSignedIdentifierHeaders, }; pub use extensions::*; + +use azure_core::error::ErrorKind; +use azure_core::http::{headers::Headers, StatusCode}; +use azure_core::Error; + +/// A Storage-specific error that provides access to HTTP response details. +/// +#[derive(Debug, Clone)] +pub struct StorageError { + /// The HTTP status code. + pub status_code: StatusCode, + /// The Storage error code. + pub error_code: StorageErrorCode, + /// The error message. + pub message: String, + /// The headers from the response. + pub headers: Headers, +} + +impl StorageError { + pub fn status_code(&self) -> StatusCode { + self.status_code + } + + pub fn error_code(&self) -> StorageErrorCode { + self.error_code.clone() + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn headers(&self) -> &Headers { + &self.headers + } +} diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index 596a2863d3..09973c273b 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -13,11 +13,10 @@ use azure_storage_blob::models::{ BlobClientGetAccountInfoResultHeaders, BlobClientGetPropertiesOptions, BlobClientGetPropertiesResultHeaders, BlobClientSetMetadataOptions, BlobClientSetPropertiesOptions, BlobClientSetTierOptions, BlockBlobClientUploadOptions, - LeaseState, + LeaseState, StorageError, }; use azure_storage_blob_test::{create_test_blob, get_blob_name, get_container_client}; -use core::error; use std::{collections::HashMap, error::Error, time::Duration}; use tokio::time; @@ -496,19 +495,8 @@ async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box let error_kind = error_response.kind(); assert!(matches!(error_kind, ErrorKind::HttpResponse { .. })); - println!("**[BEFORE Matching ErrorKind Type]**"); - // Match out of the error_kind struct - if let ErrorKind::HttpResponse { - status, - error_code, - raw_response, - .. - } = error_kind - { - println!("Status code:{}", status); - println!("Error code:{}", error_code.clone().unwrap()); - println!("Raw response:{:?}", raw_response.clone().unwrap()); - } - println!("**[AFTER Matching ErrorKind Type]**"); + let storage_error: StorageError = error_response.try_into()?; + println!("{}", storage_error); + Ok(()) } From 719bbf718d4e7889367584c2b0165e5e902a58bf Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Wed, 8 Oct 2025 16:18:28 -0700 Subject: [PATCH 4/5] Refactor to manually parse XML --- .../src/models/extensions.rs | 85 ++++++++++++++----- .../azure_storage_blob/src/models/mod.rs | 11 ++- .../azure_storage_blob/tests/blob_client.rs | 45 ++++++++++ 3 files changed, 120 insertions(+), 21 deletions(-) diff --git a/sdk/storage/azure_storage_blob/src/models/extensions.rs b/sdk/storage/azure_storage_blob/src/models/extensions.rs index f34798ee7b..c1f64cba8e 100644 --- a/sdk/storage/azure_storage_blob/src/models/extensions.rs +++ b/sdk/storage/azure_storage_blob/src/models/extensions.rs @@ -6,6 +6,7 @@ use crate::models::{ BlockBlobClientUploadOptions, PageBlobClientCreateOptions, StorageError, StorageErrorCode, }; use azure_core::{error::ErrorKind, http::headers::Headers}; +use serde_json::Value; use std::collections::HashMap; /// Augments the current options bag to only create if the Page blob does not already exist. @@ -110,39 +111,82 @@ impl From> for BlobTags { } } +use serde::Deserialize; + +/// Internal struct for deserializing Azure Storage XML error responses. +#[derive(Debug, Deserialize)] +#[serde(rename = "Error")] +struct StorageErrorXml { + #[serde(rename = "Code")] + code: String, + #[serde(rename = "Message")] + message: String, + + // Dump any unknown fields into a HashMap to avoid deserialization failures. + // For now I am using "Value" because this lets us capture any type of value. + // We can additionally get these to all be Strings, but we will need to introduce a lightweight + // deserializer to go from all possible XML field types to String (e.g. numbers, bools, etc.) + #[serde(flatten)] + additional_fields: HashMap, +} + impl TryFrom for StorageError { type Error = azure_core::Error; fn try_from(error: azure_core::Error) -> Result { - let message = error.to_string(); - match error.kind() { ErrorKind::HttpResponse { status, - error_code, raw_response, + .. } => { - let error_code = error_code.as_ref().ok_or_else(|| { + // Existence Check for Option + let raw_response = raw_response.as_ref().ok_or_else(|| { azure_core::Error::with_message( azure_core::error::ErrorKind::DataConversion, - "error_code field missing from HttpResponse.", + "Cannot convert to StorageError: raw_response is missing.", ) })?; - let headers = raw_response - .as_ref() - .map(|raw_resp| raw_resp.headers().clone()) - .unwrap_or_default(); + // Extract Headers From Raw Response + let headers = raw_response.headers().clone(); - let error_code_enum = error_code + // Parse XML Body + let body = raw_response.body(); + if body.is_empty() { + return Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "Cannot convert to StorageError: Response Body is empty.", + )); + } + let xml_error = azure_core::xml::read_xml::(body)?; + + // Validate that Error Code and Error Message Are Present + if xml_error.code.is_empty() { + return Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "XML Error Response missing 'Code' field.", + )); + } + if xml_error.message.is_empty() { + return Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "XML Error Response missing 'Message' field.", + )); + } + + // Map Error Code to StorageErrorCode Enum + let error_code_enum = xml_error + .code .parse() - .unwrap_or(StorageErrorCode::UnknownValue(error_code.clone())); + .unwrap_or(StorageErrorCode::UnknownValue(xml_error.code)); Ok(StorageError { status_code: *status, error_code: error_code_enum, - message, + message: xml_error.message, headers, + additional_error_info: xml_error.additional_fields, }) } _ => Err(azure_core::Error::with_message( @@ -155,13 +199,16 @@ impl TryFrom for StorageError { impl std::fmt::Display for StorageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Standard Error Message: {}\n", self.message)?; - writeln!(f, "Http Status Code: {}", self.status_code)?; - writeln!(f, "Storage Error Code: {}", self.error_code)?; - writeln!(f, "Response Headers:")?; - - for (name, value) in self.headers.iter() { - writeln!(f, " \"{}\": \"{}\"", name.as_str(), value.as_str())?; + writeln!(f, "HTTP Status Code: {}\n", self.status_code)?; + writeln!(f, "Error Message: {}\n", self.message)?; + writeln!(f, "Storage Error Code: {}\n", self.error_code)?; + writeln!(f, "Response Headers: {:?}\n", self.headers)?; + + if !self.additional_error_info.is_empty() { + writeln!(f, "\nAdditional Error Info:")?; + for (key, value) in &self.additional_error_info { + writeln!(f, " {}: {}", key, value)?; + } } Ok(()) diff --git a/sdk/storage/azure_storage_blob/src/models/mod.rs b/sdk/storage/azure_storage_blob/src/models/mod.rs index 7696c3718c..1b518f0287 100644 --- a/sdk/storage/azure_storage_blob/src/models/mod.rs +++ b/sdk/storage/azure_storage_blob/src/models/mod.rs @@ -87,9 +87,9 @@ pub use extensions::*; use azure_core::error::ErrorKind; use azure_core::http::{headers::Headers, StatusCode}; use azure_core::Error; +use serde_json::Value; +use std::collections::HashMap; -/// A Storage-specific error that provides access to HTTP response details. -/// #[derive(Debug, Clone)] pub struct StorageError { /// The HTTP status code. @@ -100,6 +100,8 @@ pub struct StorageError { pub message: String, /// The headers from the response. pub headers: Headers, + /// Additional fields from the error response that weren't explicitly mapped. + pub additional_error_info: HashMap, } impl StorageError { @@ -118,4 +120,9 @@ impl StorageError { pub fn headers(&self) -> &Headers { &self.headers } + + /// Returns any additional error information fields returned by the Service. + pub fn additional_error_info(&self) -> &HashMap { + &self.additional_error_info + } } diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index 09973c273b..073bccd951 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -492,11 +492,56 @@ async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box // Act let response = blob_client.download(None).await; let error_response = response.unwrap_err(); + let error_kind = error_response.kind(); assert!(matches!(error_kind, ErrorKind::HttpResponse { .. })); let storage_error: StorageError = error_response.try_into()?; + + println!("{}", storage_error); + + Ok(()) +} + +#[recorded::test] +async fn test_additional_storage_info_parsing(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = get_container_client(recording, true).await?; + let source_blob_client = container_client.blob_client(get_blob_name(recording)); + create_test_blob(&source_blob_client, None, None).await?; + + let blob_client = container_client.blob_client(get_blob_name(recording)); + + let overwrite_blob_client = container_client.blob_client(get_blob_name(recording)); + create_test_blob( + &overwrite_blob_client, + Some(RequestContent::from(b"overruled!".to_vec())), + None, + ) + .await?; + + // Inject an erroneous 'c' so we raise Copy Source Errors + let overwrite_url = format!( + "{}{}c/{}", + overwrite_blob_client.endpoint(), + overwrite_blob_client.container_name(), + overwrite_blob_client.blob_name() + ); + + // Copy Source Error Scenario + let response = blob_client + .block_blob_client() + .upload_blob_from_url(overwrite_url.clone(), None) + .await; + // Assert + let error = response.unwrap_err(); + assert_eq!(StatusCode::NotFound, error.http_status().unwrap()); + + let storage_error: StorageError = error.try_into()?; + println!("{}", storage_error); + container_client.delete_container(None).await?; Ok(()) } From 5436e90a5134dc32819bfdc724142b100bd3cc58 Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Fri, 10 Oct 2025 16:54:39 -0700 Subject: [PATCH 5/5] Move StorageError to separate file, move StorageErrorXML to be internal, exhaustively match possible XML value nodes --- .../src/models/extensions.rs | 108 +------- .../azure_storage_blob/src/models/mod.rs | 45 +--- .../src/models/storage_error.rs | 243 ++++++++++++++++++ .../azure_storage_blob/tests/blob_client.rs | 21 ++ 4 files changed, 267 insertions(+), 150 deletions(-) create mode 100644 sdk/storage/azure_storage_blob/src/models/storage_error.rs diff --git a/sdk/storage/azure_storage_blob/src/models/extensions.rs b/sdk/storage/azure_storage_blob/src/models/extensions.rs index c1f64cba8e..6a3a0683db 100644 --- a/sdk/storage/azure_storage_blob/src/models/extensions.rs +++ b/sdk/storage/azure_storage_blob/src/models/extensions.rs @@ -3,10 +3,8 @@ use crate::models::{ AppendBlobClientCreateOptions, BlobTag, BlobTags, BlockBlobClientUploadBlobFromUrlOptions, - BlockBlobClientUploadOptions, PageBlobClientCreateOptions, StorageError, StorageErrorCode, + BlockBlobClientUploadOptions, PageBlobClientCreateOptions, }; -use azure_core::{error::ErrorKind, http::headers::Headers}; -use serde_json::Value; use std::collections::HashMap; /// Augments the current options bag to only create if the Page blob does not already exist. @@ -110,107 +108,3 @@ impl From> for BlobTags { } } } - -use serde::Deserialize; - -/// Internal struct for deserializing Azure Storage XML error responses. -#[derive(Debug, Deserialize)] -#[serde(rename = "Error")] -struct StorageErrorXml { - #[serde(rename = "Code")] - code: String, - #[serde(rename = "Message")] - message: String, - - // Dump any unknown fields into a HashMap to avoid deserialization failures. - // For now I am using "Value" because this lets us capture any type of value. - // We can additionally get these to all be Strings, but we will need to introduce a lightweight - // deserializer to go from all possible XML field types to String (e.g. numbers, bools, etc.) - #[serde(flatten)] - additional_fields: HashMap, -} - -impl TryFrom for StorageError { - type Error = azure_core::Error; - - fn try_from(error: azure_core::Error) -> Result { - match error.kind() { - ErrorKind::HttpResponse { - status, - raw_response, - .. - } => { - // Existence Check for Option - let raw_response = raw_response.as_ref().ok_or_else(|| { - azure_core::Error::with_message( - azure_core::error::ErrorKind::DataConversion, - "Cannot convert to StorageError: raw_response is missing.", - ) - })?; - - // Extract Headers From Raw Response - let headers = raw_response.headers().clone(); - - // Parse XML Body - let body = raw_response.body(); - if body.is_empty() { - return Err(azure_core::Error::with_message( - azure_core::error::ErrorKind::DataConversion, - "Cannot convert to StorageError: Response Body is empty.", - )); - } - let xml_error = azure_core::xml::read_xml::(body)?; - - // Validate that Error Code and Error Message Are Present - if xml_error.code.is_empty() { - return Err(azure_core::Error::with_message( - azure_core::error::ErrorKind::DataConversion, - "XML Error Response missing 'Code' field.", - )); - } - if xml_error.message.is_empty() { - return Err(azure_core::Error::with_message( - azure_core::error::ErrorKind::DataConversion, - "XML Error Response missing 'Message' field.", - )); - } - - // Map Error Code to StorageErrorCode Enum - let error_code_enum = xml_error - .code - .parse() - .unwrap_or(StorageErrorCode::UnknownValue(xml_error.code)); - - Ok(StorageError { - status_code: *status, - error_code: error_code_enum, - message: xml_error.message, - headers, - additional_error_info: xml_error.additional_fields, - }) - } - _ => Err(azure_core::Error::with_message( - azure_core::error::ErrorKind::DataConversion, - "ErrorKind was not HttpResponse and could not be parsed.", - )), - } - } -} - -impl std::fmt::Display for StorageError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "HTTP Status Code: {}\n", self.status_code)?; - writeln!(f, "Error Message: {}\n", self.message)?; - writeln!(f, "Storage Error Code: {}\n", self.error_code)?; - writeln!(f, "Response Headers: {:?}\n", self.headers)?; - - if !self.additional_error_info.is_empty() { - writeln!(f, "\nAdditional Error Info:")?; - for (key, value) in &self.additional_error_info { - writeln!(f, " {}: {}", key, value)?; - } - } - - Ok(()) - } -} diff --git a/sdk/storage/azure_storage_blob/src/models/mod.rs b/sdk/storage/azure_storage_blob/src/models/mod.rs index 1b518f0287..9fa8c63d0a 100644 --- a/sdk/storage/azure_storage_blob/src/models/mod.rs +++ b/sdk/storage/azure_storage_blob/src/models/mod.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. mod extensions; +mod storage_error; pub use crate::generated::models::{ AccessTier, AccountKind, AppendBlobClientAppendBlockFromUrlOptions, @@ -83,46 +84,4 @@ pub use crate::generated::models::{ VecSignedIdentifierHeaders, }; pub use extensions::*; - -use azure_core::error::ErrorKind; -use azure_core::http::{headers::Headers, StatusCode}; -use azure_core::Error; -use serde_json::Value; -use std::collections::HashMap; - -#[derive(Debug, Clone)] -pub struct StorageError { - /// The HTTP status code. - pub status_code: StatusCode, - /// The Storage error code. - pub error_code: StorageErrorCode, - /// The error message. - pub message: String, - /// The headers from the response. - pub headers: Headers, - /// Additional fields from the error response that weren't explicitly mapped. - pub additional_error_info: HashMap, -} - -impl StorageError { - pub fn status_code(&self) -> StatusCode { - self.status_code - } - - pub fn error_code(&self) -> StorageErrorCode { - self.error_code.clone() - } - - pub fn message(&self) -> &str { - &self.message - } - - pub fn headers(&self) -> &Headers { - &self.headers - } - - /// Returns any additional error information fields returned by the Service. - pub fn additional_error_info(&self) -> &HashMap { - &self.additional_error_info - } -} +pub use storage_error::StorageError; diff --git a/sdk/storage/azure_storage_blob/src/models/storage_error.rs b/sdk/storage/azure_storage_blob/src/models/storage_error.rs new file mode 100644 index 0000000000..6d8951721b --- /dev/null +++ b/sdk/storage/azure_storage_blob/src/models/storage_error.rs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::generated::models::StorageErrorCode; +use azure_core::error::ErrorKind; +use azure_core::http::RawResponse; +use serde::Deserialize; +use serde_json::Value; +use std::collections::HashMap; + +/// Represents an error response from Azure Storage services. +#[derive(Debug, Clone)] +pub struct StorageError { + /// The HTTP status code. + pub status_code: azure_core::http::StatusCode, + /// The Storage error code, if available. + pub error_code: Option, + /// The error message, if available. + pub message: Option, + /// The request ID from the x-ms-request-id header, if available. + pub request_id: Option, + /// The raw HTTP response. + pub raw_response: RawResponse, + /// Additional fields from the error response that weren't explicitly mapped. + pub additional_error_info: HashMap, +} + +impl StorageError { + pub fn status_code(&self) -> azure_core::http::StatusCode { + self.status_code + } + + pub fn error_code(&self) -> Option<&StorageErrorCode> { + self.error_code.as_ref() + } + + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } + + pub fn request_id(&self) -> Option<&str> { + self.request_id.as_deref() + } + + pub fn raw_response(&self) -> &RawResponse { + &self.raw_response + } + + pub fn additional_error_info(&self) -> &HashMap { + &self.additional_error_info + } + + /// Converts a `serde_json::Value` to a String representation, handling nested XML structures. + fn value_to_string(value: &Value) -> String { + match value { + // Handle null values + Value::Null => "null".to_string(), + + // Handle boolean values + Value::Bool(b) => b.to_string(), + + // Handle numeric values + Value::Number(n) => n.to_string(), + + // Handle string values + Value::String(s) => s.clone(), + + // Handle arrays + Value::Array(arr) => { + let elements: Vec = arr.iter().map(Self::value_to_string).collect(); + format!("[{}]", elements.join(", ")) + } + + // Handle objects (including XML elements with $text) + Value::Object(obj) => { + // Special case: if the object only has a "$text" field, extract it + if obj.len() == 1 && obj.contains_key("$text") { + if let Some(Value::String(text)) = obj.get("$text") { + return text.clone(); + } + } + + // For other objects, format as key-value pairs + let pairs: Vec = obj + .iter() + .map(|(k, v)| { + // Skip $text key in compound objects to avoid duplication + if k == "$text" { + Self::value_to_string(v) + } else { + format!("{}: {}", k, Self::value_to_string(v)) + } + }) + .collect(); + + // If it's a single element (after filtering), return it directly + if pairs.len() == 1 { + pairs[0].clone() + } else { + format!("{{{}}}", pairs.join(", ")) + } + } + } + } + + /// Deserializes a `StorageError` from XML body with HTTP response metadata. + fn from_xml( + xml_body: &[u8], + status_code: azure_core::http::StatusCode, + raw_response: RawResponse, + ) -> Result { + #[derive(Deserialize)] + #[serde(rename = "Error")] + struct StorageErrorXml { + #[serde(rename = "Code")] + code: Option, + #[serde(rename = "Message")] + message: Option, + #[serde(flatten)] + additional_fields: HashMap, + } + + let xml_fields = azure_core::xml::read_xml::(xml_body)?; + + // Parse error code from XML body + let error_code = xml_fields.code.and_then(|code| { + code.parse() + .ok() + .or(Some(StorageErrorCode::UnknownValue(code))) + }); + + let request_id = raw_response.headers().get_optional_string( + &azure_core::http::headers::HeaderName::from_static("x-ms-request-id"), + ); + + // Convert additional fields from HashMap to HashMap + let additional_error_info = xml_fields + .additional_fields + .iter() + .map(|(k, v)| (k.clone(), Self::value_to_string(v))) + .collect(); + + Ok(StorageError { + status_code, + error_code, + message: xml_fields.message, + request_id, + raw_response, + additional_error_info, + }) + } +} + +impl TryFrom for StorageError { + type Error = azure_core::Error; + + fn try_from(error: azure_core::Error) -> Result { + match error.kind() { + ErrorKind::HttpResponse { + status, + error_code, + raw_response, + } => { + let raw_response = raw_response.as_ref().ok_or_else(|| { + azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "Cannot convert to StorageError: raw_response is missing.", + ) + })?; + + let body = raw_response.body(); + + // No XML Body, Use Error Code from HttpResponse + + // TODO: Need to figure out how to extract the message, seems like it's getting dropped somewhere when azure_core::Error model is created + // i.e. Captured Response from Over The Wire: + // HTTP/1.1 404 The specified blob does not exist. <---- This is getting dropped, I don't see it in Error or HttpResponse debug outputs + // Transfer-Encoding: chunked + // Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0 + // x-ms-request-id: xxxx + // x-ms-client-request-id: xxxxxx + // x-ms-version: 2025-11-05 + // x-ms-error-code: BlobNotFound + // Date: Fri, 10 Oct 2025 22:55:41 GMT + + if body.is_empty() { + let error_code = error_code.as_ref().and_then(|code| { + code.parse() + .ok() + .or(Some(StorageErrorCode::UnknownValue(code.clone()))) + }); + + let request_id = raw_response.as_ref().clone().headers().get_optional_string( + &azure_core::http::headers::HeaderName::from_static("x-ms-request-id"), + ); + + return Ok(StorageError { + status_code: *status, + error_code, + message: None, + request_id, + raw_response: raw_response.as_ref().clone(), + additional_error_info: HashMap::new(), + }); + } + + StorageError::from_xml(body, *status, raw_response.as_ref().clone()) + } + // TODO: We may have to handle other ErrorKind variants, but catch-all for now. + _ => Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "ErrorKind was not HttpResponse and could not be parsed.", + )), + } + } +} + +impl std::fmt::Display for StorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "HTTP Status Code: {}", self.status_code)?; + + if let Some(request_id) = &self.request_id { + writeln!(f, "Request ID: {}", request_id)?; + } + + if let Some(error_code) = &self.error_code { + writeln!(f, "Storage Error Code: {}", error_code)?; + } + + if let Some(message) = &self.message { + writeln!(f, "Error Message: {}", message)?; + } + + if !self.additional_error_info.is_empty() { + writeln!(f, "\nAdditional Error Info:")?; + for (key, value) in &self.additional_error_info { + writeln!(f, "{}: {}", key, value)?; + } + } + + Ok(()) + } +} diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index 073bccd951..a12e57f9c3 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -503,6 +503,27 @@ async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box Ok(()) } +#[recorded::test] +async fn test_bodyless_storage_error_model(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = get_container_client(recording, true).await?; + let blob_client = container_client.blob_client(get_blob_name(recording)); + + // Act + let response = blob_client.get_properties(None).await; + let error_response = response.unwrap_err(); + + let error_kind = error_response.kind(); + assert!(matches!(error_kind, ErrorKind::HttpResponse { .. })); + + let storage_error: StorageError = error_response.try_into()?; + + println!("{}", storage_error); + + Ok(()) +} + #[recorded::test] async fn test_additional_storage_info_parsing(ctx: TestContext) -> Result<(), Box> { // Recording Setup