use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;
use azure_data_cosmos_driver::error::CosmosError as DriverCosmosError;
use azure_data_cosmos_driver::models::CosmosResponse;
use crate::models::DiagnosticsContext;
pub type CosmosStatus = azure_data_cosmos_driver::error::CosmosStatus;
pub type SubStatusCode = azure_data_cosmos_driver::error::SubStatusCode;
#[repr(transparent)]
#[derive(Clone)]
pub struct CosmosError(DriverCosmosError);
impl CosmosError {
pub fn status(&self) -> CosmosStatus {
self.0.status()
}
pub fn response(&self) -> Option<&CosmosResponse> {
self.0.response()
}
pub fn diagnostics(&self) -> Option<Arc<DiagnosticsContext>> {
self.0.diagnostics()
}
}
impl fmt::Display for CosmosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl fmt::Debug for CosmosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl StdError for CosmosError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.0.source()
}
}
impl From<DriverCosmosError> for CosmosError {
fn from(inner: DriverCosmosError) -> Self {
Self(inner)
}
}
impl From<serde_json::Error> for CosmosError {
fn from(error: serde_json::Error) -> Self {
Self(
DriverCosmosError::builder()
.with_status(CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("JSON serialization or deserialization failed")
.with_source(error)
.build(),
)
}
}
impl From<url::ParseError> for CosmosError {
fn from(error: url::ParseError) -> Self {
Self(
DriverCosmosError::builder()
.with_status(CosmosStatus::CLIENT_INVALID_URL)
.with_message("invalid URL")
.with_source(error)
.build(),
)
}
}
impl From<CosmosError> for azure_core::Error {
fn from(err: CosmosError) -> Self {
let core_kind = classify_for_azure_core(&err);
azure_core::Error::new(core_kind, err)
}
}
fn classify_for_azure_core(err: &CosmosError) -> azure_core::error::ErrorKind {
use azure_core::error::ErrorKind as CoreKind;
let status = err.status();
let sub = status.sub_status();
if let Some(resp) = err.response() {
use azure_data_cosmos_driver::models::ResponseBody;
let raw_response = match resp.body() {
ResponseBody::Bytes(b) => Some(Box::new(azure_core::http::RawResponse::from_bytes(
status.status_code(),
resp.headers().to_raw_headers(),
b.clone(),
))),
ResponseBody::NoPayload => Some(Box::new(azure_core::http::RawResponse::from_bytes(
status.status_code(),
resp.headers().to_raw_headers(),
azure_core::Bytes::new(),
))),
ResponseBody::Items(_) => None,
};
return CoreKind::HttpResponse {
status: status.status_code(),
error_code: sub.map(|s| s.value().to_string()),
raw_response,
};
}
match sub {
Some(SubStatusCode::AUTHENTICATION_TOKEN_ACQUISITION_FAILED)
| Some(SubStatusCode::CLIENT_GENERATED_401) => CoreKind::Credential,
Some(SubStatusCode::SERIALIZATION_RESPONSE_BODY_INVALID) => CoreKind::DataConversion,
Some(SubStatusCode::TRANSPORT_CONNECTION_FAILED)
| Some(SubStatusCode::TRANSPORT_DNS_FAILED)
| Some(SubStatusCode::TRANSPORT_HTTP2_INCOMPATIBLE) => CoreKind::Connection,
Some(SubStatusCode::TRANSPORT_IO_FAILED)
| Some(SubStatusCode::TRANSPORT_BODY_READ_FAILED)
| Some(SubStatusCode::TRANSPORT_GENERATED_503)
| Some(SubStatusCode::CLIENT_OPERATION_TIMEOUT) => CoreKind::Io,
_ => CoreKind::Other,
}
}
pub type Result<T> = std::result::Result<T, CosmosError>;
#[cfg(test)]
mod tests {
use super::*;
use azure_core::error::ErrorKind as CoreErrorKind;
#[test]
fn from_cosmos_error_for_azure_core_error_preserves_chain_and_kind() {
let inner_io = std::io::Error::new(std::io::ErrorKind::Other, "io fail");
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::TRANSPORT_IO_FAILED)
.with_message("transport blew up")
.with_source(inner_io)
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(matches!(core_err.kind(), CoreErrorKind::Io));
let rendered = format!("{core_err}");
assert!(
rendered.contains("transport blew up") || rendered.contains("io fail"),
"azure_core::Error rendering must surface the cosmos message or chain: {rendered}",
);
}
#[test]
fn from_cosmos_error_for_azure_core_error_maps_dns_failure_to_connection() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::TRANSPORT_DNS_FAILED)
.with_message("dns lookup failed")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(
matches!(core_err.kind(), CoreErrorKind::Connection),
"TRANSPORT_DNS_FAILED must map to Connection, got {:?}",
core_err.kind()
);
}
#[test]
fn from_cosmos_error_for_azure_core_error_maps_auth_to_credential() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::AUTHENTICATION_TOKEN_ACQUISITION_FAILED)
.with_message("token acquisition failed")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(matches!(core_err.kind(), CoreErrorKind::Credential));
}
#[test]
fn from_cosmos_error_for_azure_core_error_maps_serialization_to_data_conversion() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("bad json")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(matches!(core_err.kind(), CoreErrorKind::DataConversion));
}
#[test]
fn from_cosmos_error_for_azure_core_error_synthetic_without_substatus_is_other() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::new(azure_core::http::StatusCode::BadRequest))
.with_message("bad arg")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(matches!(core_err.kind(), CoreErrorKind::Other));
}
#[test]
fn from_cosmos_error_for_azure_core_error_downcast_recovers_cosmos_error() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::new(azure_core::http::StatusCode::BadRequest))
.with_message("bad arg")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
let chain: &(dyn std::error::Error + 'static) = &core_err;
let mut cur = chain.source();
let mut found = false;
while let Some(s) = cur {
if s.downcast_ref::<CosmosError>().is_some() {
found = true;
break;
}
cur = s.source();
}
assert!(
found,
"azure_core::Error source chain must let callers downcast back to CosmosError"
);
}
#[test]
fn from_cosmos_error_for_azure_core_error_connection_siblings_all_map_to_connection() {
for status in [
CosmosStatus::TRANSPORT_CONNECTION_FAILED,
CosmosStatus::TRANSPORT_HTTP2_INCOMPATIBLE,
] {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(status)
.with_message("never sent")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(
matches!(core_err.kind(), CoreErrorKind::Connection),
"{:?} must map to Connection, got {:?}",
status.sub_status(),
core_err.kind()
);
}
}
#[test]
fn from_cosmos_error_for_azure_core_error_io_siblings_all_map_to_io() {
for status in [
CosmosStatus::TRANSPORT_BODY_READ_FAILED,
CosmosStatus::TRANSPORT_GENERATED_503,
] {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(status)
.with_message("mid-stream")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(
matches!(core_err.kind(), CoreErrorKind::Io),
"{:?} must map to Io, got {:?}",
status.sub_status(),
core_err.kind()
);
}
}
#[test]
fn from_cosmos_error_for_azure_core_error_client_generated_401_maps_to_credential() {
let cosmos: CosmosError = DriverCosmosError::builder()
.with_status(CosmosStatus::CLIENT_GENERATED_401)
.with_message("client-side auth failure")
.build()
.into();
let core_err: azure_core::Error = cosmos.into();
assert!(
matches!(core_err.kind(), CoreErrorKind::Credential),
"CLIENT_GENERATED_401 must map to Credential, got {:?}",
core_err.kind()
);
}
}