Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 107 additions & 2 deletions sdk/storage/azure_storage_blob/src/models/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -109,3 +110,107 @@ impl From<HashMap<String, String>> 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<String, Value>,
}

impl TryFrom<azure_core::Error> for StorageError {
type Error = azure_core::Error;

fn try_from(error: azure_core::Error) -> Result<Self, Self::Error> {
match error.kind() {
ErrorKind::HttpResponse {
status,
raw_response,
..
} => {
// Existence Check for Option<RawResponse>
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::<StorageErrorXml>(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(())
}
}
43 changes: 43 additions & 0 deletions sdk/storage/azure_storage_blob/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Value>,
}

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<String, Value> {
&self.additional_error_info
}
}
67 changes: 66 additions & 1 deletion sdk/storage/azure_storage_blob/tests/blob_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use azure_core::{
error::ErrorKind,
http::{RequestContent, StatusCode},
Bytes,
};
Expand All @@ -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};
Expand Down Expand Up @@ -480,3 +481,67 @@ async fn test_get_account_info(ctx: TestContext) -> Result<(), Box<dyn Error>> {

Ok(())
}

#[recorded::test]
async fn test_storage_error_model(ctx: TestContext) -> Result<(), Box<dyn Error>> {
// 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<dyn Error>> {
// 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(())
}
Loading