diff --git a/sdk/storage/azure_storage_blob/src/models/extensions.rs b/sdk/storage/azure_storage_blob/src/models/extensions.rs index 03246605a8..c1f64cba8e 100644 --- a/sdk/storage/azure_storage_blob/src/models/extensions.rs +++ b/sdk/storage/azure_storage_blob/src/models/extensions.rs @@ -3,9 +3,10 @@ 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 serde_json::Value; use std::collections::HashMap; /// Augments the current options bag to only create if the Page blob does not already exist. @@ -109,3 +110,107 @@ 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 25f0e07366..1b518f0287 100644 --- a/sdk/storage/azure_storage_blob/src/models/mod.rs +++ b/sdk/storage/azure_storage_blob/src/models/mod.rs @@ -83,3 +83,46 @@ 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 + } +} diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index 7301e86ac7..073bccd951 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, }; @@ -12,7 +13,7 @@ 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}; @@ -480,3 +481,67 @@ 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 + 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.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(()) +}