use std::error::Error;
use std::fmt::{self, Debug, Display, Formatter};
use crate::error::{ErrorEnvelope, ServiceError, canonical_http_status, new_error_instance_id};
pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
pub struct ServiceFailure<E> {
envelope: ErrorEnvelope,
declared: Option<E>,
observed_status: Option<u16>,
}
impl<E> ServiceFailure<E> {
pub fn undeclared(envelope: ErrorEnvelope) -> Self {
Self {
envelope,
declared: None,
observed_status: None,
}
}
pub fn envelope(&self) -> &ErrorEnvelope {
&self.envelope
}
pub fn declared(&self) -> Option<&E> {
self.declared.as_ref()
}
pub fn into_declared(mut self) -> Result<E, Self> {
match self.declared.take() {
Some(error) => Ok(error),
None => Err(self),
}
}
pub fn canonical_status(&self) -> u16 {
canonical_http_status(&self.envelope.error_code)
}
pub fn observed_status(&self) -> Option<u16> {
self.observed_status
}
pub fn status(&self) -> u16 {
self.observed_status
.unwrap_or_else(|| self.canonical_status())
}
fn into_local_undeclared<T>(self) -> ServiceFailure<T> {
ServiceFailure {
envelope: self.envelope,
declared: None,
observed_status: None,
}
}
}
impl<E: ServiceError> ServiceFailure<E> {
fn locally_declared(error: E) -> Self {
let envelope = error.to_envelope(new_error_instance_id());
Self {
envelope,
declared: Some(error),
observed_status: None,
}
}
pub fn remote(status: u16, envelope: ErrorEnvelope) -> Self {
let declared = (canonical_http_status(&envelope.error_code) == status)
.then(|| E::from_envelope(&envelope))
.flatten();
Self {
envelope,
declared,
observed_status: Some(status),
}
}
}
pub struct UnknownRemote {
status: u16,
content_type: Option<String>,
body: Vec<u8>,
truncated: bool,
source: Option<BoxError>,
}
impl UnknownRemote {
pub fn status(&self) -> u16 {
self.status
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn truncated(&self) -> bool {
self.truncated
}
}
pub struct TransportFailure {
source: BoxError,
observed_status: Option<u16>,
}
impl TransportFailure {
pub fn observed_status(&self) -> Option<u16> {
self.observed_status
}
}
pub struct ProtocolFailure {
message: String,
observed_status: Option<u16>,
}
impl ProtocolFailure {
pub fn message(&self) -> &str {
&self.message
}
pub fn observed_status(&self) -> Option<u16> {
self.observed_status
}
}
pub enum CallError<E> {
Service(ServiceFailure<E>),
UnknownRemote(UnknownRemote),
Transport(TransportFailure),
Protocol(ProtocolFailure),
Internal {
source: BoxError,
error_instance_id: String,
},
}
impl<E> CallError<E> {
pub fn transport(error: impl Into<BoxError>) -> Self {
Self::Transport(TransportFailure {
source: error.into(),
observed_status: None,
})
}
pub fn transport_response(status: u16, error: impl Into<BoxError>) -> Self {
Self::Transport(TransportFailure {
source: error.into(),
observed_status: Some(status),
})
}
pub fn protocol(message: impl Into<String>) -> Self {
Self::Protocol(ProtocolFailure {
message: message.into(),
observed_status: None,
})
}
pub fn protocol_response(status: u16, message: impl Into<String>) -> Self {
Self::Protocol(ProtocolFailure {
message: message.into(),
observed_status: Some(status),
})
}
pub fn unknown_remote(
status: u16,
content_type: Option<String>,
body: Vec<u8>,
truncated: bool,
) -> Self {
Self::UnknownRemote(UnknownRemote {
status,
content_type,
body,
truncated,
source: None,
})
}
pub fn unreadable_remote(
status: u16,
content_type: Option<String>,
body: Vec<u8>,
error: impl Into<BoxError>,
) -> Self {
Self::UnknownRemote(UnknownRemote {
status,
content_type,
body,
truncated: false,
source: Some(error.into()),
})
}
pub fn service(envelope: ErrorEnvelope) -> Self {
Self::Service(ServiceFailure::undeclared(envelope))
}
pub fn internal(error: impl Into<BoxError>) -> Self {
Self::Internal {
source: error.into(),
error_instance_id: new_error_instance_id(),
}
}
pub fn unauthenticated() -> Self {
Self::service(ErrorEnvelope::new(
"UNAUTHENTICATED",
"Default:Unauthenticated",
new_error_instance_id(),
))
}
pub fn permission_denied(operation: impl Into<String>, resource: impl Into<String>) -> Self {
let mut envelope = ErrorEnvelope::new(
"PERMISSION_DENIED",
"Default:PermissionDenied",
new_error_instance_id(),
);
envelope
.parameters
.insert("operation".into(), operation.into());
envelope
.parameters
.insert("resource".into(), resource.into());
Self::service(envelope)
}
pub fn error_instance_id(&self) -> Option<&str> {
match self {
Self::Service(failure) => Some(&failure.envelope.error_instance_id),
Self::Internal {
error_instance_id, ..
} => Some(error_instance_id),
Self::UnknownRemote(_) | Self::Transport(_) | Self::Protocol(_) => None,
}
}
pub fn from_dependency<F>(error: CallError<F>) -> Self
where
F: Send + Sync + 'static,
{
match error {
CallError::Service(failure) if matches!(failure.canonical_status(), 401 | 403) => {
Self::Service(failure.into_local_undeclared())
}
error => {
let error_instance_id = error
.error_instance_id()
.map(ToOwned::to_owned)
.unwrap_or_else(new_error_instance_id);
Self::Internal {
source: Box::new(error),
error_instance_id,
}
}
}
}
}
impl<E: ServiceError> From<E> for CallError<E> {
fn from(error: E) -> Self {
Self::Service(ServiceFailure::locally_declared(error))
}
}
impl<E> Display for CallError<E> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Service(_) => "service call rejected",
Self::UnknownRemote(_) => "remote service returned an unknown error",
Self::Transport(_) => "service transport failed",
Self::Protocol(_) => "invalid service response",
Self::Internal { .. } => "service implementation failed",
})
}
}
impl<E> Debug for CallError<E> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Service(_) => "CallError::Service([REDACTED])",
Self::UnknownRemote(_) => "CallError::UnknownRemote([REDACTED])",
Self::Transport(_) => "CallError::Transport([REDACTED])",
Self::Protocol(_) => "CallError::Protocol([REDACTED])",
Self::Internal { .. } => "CallError::Internal([REDACTED])",
})
}
}
impl<E> Error for CallError<E>
where
E: Send + Sync + 'static,
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Service(failure) => Some(&failure.envelope),
Self::UnknownRemote(error) => error.source.as_deref().map(|error| error as _),
Self::Transport(error) => Some(error.source.as_ref()),
Self::Protocol(_) => None,
Self::Internal { source, .. } => Some(source.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::io;
use uuid::Uuid;
use super::{CallError, ServiceFailure};
use crate::error::{ErrorEnvelope, ServiceError};
#[derive(Debug)]
struct TestError;
impl ServiceError for TestError {
fn to_envelope(&self, instance: String) -> ErrorEnvelope {
ErrorEnvelope::new("NOT_FOUND", "Test:NotFound", instance)
}
fn from_envelope(envelope: &ErrorEnvelope) -> Option<Self> {
(envelope.error_code == "NOT_FOUND" && envelope.error_name == "Test:NotFound")
.then_some(Self)
}
}
#[test]
fn hides_diagnostics_but_preserves_error_sources() {
let errors = [
CallError::<TestError>::transport(io::Error::other("secret")),
CallError::<TestError>::protocol("secret"),
CallError::<TestError>::internal(io::Error::other("secret")),
];
for error in errors {
assert!(!error.to_string().contains("secret"));
assert!(!format!("{error:?}").contains("secret"));
if !matches!(error, CallError::Protocol(_)) {
assert_eq!(error.source().unwrap().to_string(), "secret");
}
}
}
#[test]
fn dependency_propagation_keeps_only_canonical_auth_failures() {
let denied = CallError::<TestError>::permission_denied("read", "resource");
let instance = denied.error_instance_id().unwrap().to_owned();
let propagated = CallError::<TestError>::from_dependency(denied);
let CallError::Service(failure) = propagated else {
panic!("permission denial was not propagated")
};
assert_eq!(failure.canonical_status(), 403);
assert_eq!(failure.status(), 403);
assert_eq!(failure.observed_status(), None);
assert_eq!(failure.envelope().error_instance_id, instance);
assert!(failure.declared().is_none());
let envelope = ErrorEnvelope::new(
"PERMISSION_DENIED",
"Default:PermissionDenied",
Uuid::now_v7().to_string(),
);
let remote = CallError::<TestError>::Service(ServiceFailure::remote(500, envelope));
let CallError::Service(failure) = CallError::<TestError>::from_dependency(remote) else {
panic!("canonical permission denial was not propagated")
};
assert_eq!(failure.status(), 403);
assert_eq!(failure.observed_status(), None);
let remote = CallError::<TestError>::unknown_remote(401, None, Vec::new(), false);
assert!(matches!(
CallError::<TestError>::from_dependency(remote),
CallError::Internal { .. }
));
}
}