use crate::audit::AuditRecord;
use crate::core::{AsxError, ErrorCode};
use std::fmt;
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
message: String,
retryable: bool,
ebms: Option<String>,
mdn: Option<String>,
evidence: Option<Box<AuditRecord>>,
source: Option<BoxError>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Config,
Rejected,
PartnerError,
Transport,
Indeterminate,
Backend,
}
impl ErrorKind {
fn as_str(self) -> &'static str {
match self {
Self::Config => "configuration error",
Self::Rejected => "rejected",
Self::PartnerError => "partner error",
Self::Transport => "transport error",
Self::Indeterminate => "indeterminate",
Self::Backend => "backend error",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(any(feature = "as2", feature = "as4")), allow(dead_code))]
pub(crate) enum Stage {
Config,
HttpNotWritten,
HttpAfterWrite,
ResponseVerify,
AsyncMdnMatch,
SmpLookup,
Inbound,
}
impl Error {
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn is_retryable(&self) -> bool {
self.retryable
}
pub fn ebms_code(&self) -> Option<&str> {
self.ebms.as_deref()
}
pub fn mdn_disposition(&self) -> Option<&str> {
self.mdn.as_deref()
}
pub fn evidence(&self) -> Option<&AuditRecord> {
self.evidence.as_deref()
}
pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
let retryable = matches!(
kind,
ErrorKind::Transport | ErrorKind::Indeterminate | ErrorKind::Backend
);
Self {
kind,
message: message.into(),
retryable,
ebms: None,
mdn: None,
evidence: None,
source: None,
}
}
pub(crate) fn config(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Config, message)
}
pub(crate) fn backend(message: impl Into<String>, source: BoxError) -> Self {
Self::new(ErrorKind::Backend, message).with_source(source)
}
pub(crate) fn partner_ebms(code: impl Into<String>, message: impl Into<String>) -> Self {
let code = code.into();
let mut err = Self::new(ErrorKind::PartnerError, message);
err.retryable = code == "EBMS:0202";
err.ebms = Some(code);
err
}
#[cfg_attr(not(feature = "as2"), allow(dead_code))]
pub(crate) fn partner_mdn(disposition: impl Into<String>, message: impl Into<String>) -> Self {
let disposition = disposition.into();
let mut err = Self::new(ErrorKind::PartnerError, message);
err.retryable = disposition.contains("unexpected-processing-error");
err.mdn = Some(disposition);
err
}
#[cfg_attr(not(any(feature = "as2", feature = "as4")), allow(dead_code))]
pub(crate) fn retryable_if(mut self, condition: bool) -> Self {
self.retryable |= condition;
self
}
pub(crate) fn with_source(mut self, source: BoxError) -> Self {
self.source = Some(source);
self
}
pub(crate) fn with_evidence(mut self, record: Option<AuditRecord>) -> Self {
self.evidence = record.map(Box::new);
self
}
pub(crate) fn delivered_but_unrecorded(source: Self, record: AuditRecord) -> Self {
let mut err = Self::new(
ErrorKind::Backend,
format!("the message was delivered but its audit record was not written: {source}"),
);
err.retryable = false;
err.evidence = Some(Box::new(record));
err.source = Some(Box::new(source));
err
}
pub(crate) fn internal(err: AsxError) -> Self {
Self::from_internal(Stage::Config, err)
}
pub(crate) fn from_internal(stage: Stage, err: AsxError) -> Self {
let kind = match (stage, err.code) {
(_, ErrorCode::CapacityExhausted) => ErrorKind::Transport,
(_, ErrorCode::StorageBackendFailure | ErrorCode::ReliabilityFailure) => {
ErrorKind::Backend
}
(Stage::Config, _) => ErrorKind::Config,
(Stage::HttpNotWritten, ErrorCode::TransportFailure) => ErrorKind::Transport,
(
Stage::HttpNotWritten,
ErrorCode::InvalidInput | ErrorCode::PolicyViolation | ErrorCode::NotFound,
) => ErrorKind::Transport,
(Stage::HttpNotWritten | Stage::HttpAfterWrite, _) => ErrorKind::Indeterminate,
(Stage::ResponseVerify, _) => ErrorKind::Indeterminate,
(Stage::AsyncMdnMatch, _) => ErrorKind::Rejected,
(Stage::SmpLookup, ErrorCode::TransportFailure | ErrorCode::Timeout) => {
ErrorKind::Transport
}
(Stage::SmpLookup, ErrorCode::NotFound | ErrorCode::InvalidInput) => ErrorKind::Config,
(Stage::SmpLookup, _) => ErrorKind::Rejected,
(Stage::Inbound, _) => ErrorKind::Rejected,
};
let mut out = Self::new(kind, err.message.clone());
let address_pin_refusal = stage == Stage::HttpNotWritten
&& matches!(
err.code,
ErrorCode::InvalidInput | ErrorCode::PolicyViolation | ErrorCode::NotFound
);
out.retryable = match kind {
ErrorKind::Transport => !address_pin_refusal,
ErrorKind::Indeterminate | ErrorKind::Backend => true,
_ => false,
};
out.source = Some(Box::new(err));
out
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind.as_str(), self.message)?;
if let Some(code) = &self.ebms {
write!(f, " ({code})")?;
}
if let Some(disposition) = &self.mdn {
write!(f, " ({disposition})")?;
}
Ok(())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|s| s as &(dyn std::error::Error + 'static))
}
}
#[doc(hidden)]
pub mod __mapping {
use super::*;
use crate::core::ErrorContext;
#[doc(hidden)]
pub fn map(stage: &str, code: ErrorCode) -> Option<Error> {
let stage = match stage {
"config" => Stage::Config,
"http_not_written" => Stage::HttpNotWritten,
"http_after_write" => Stage::HttpAfterWrite,
"response_verify" => Stage::ResponseVerify,
"async_mdn_match" => Stage::AsyncMdnMatch,
"smp_lookup" => Stage::SmpLookup,
"inbound" => Stage::Inbound,
_ => return None,
};
Some(Error::from_internal(
stage,
AsxError::new(code, "mapped", ErrorContext::new("error_mapping")),
))
}
#[doc(hidden)]
pub fn partner_ebms(code: &str) -> Error {
Error::partner_ebms(code, "partner refused")
}
#[doc(hidden)]
pub fn partner_mdn(disposition: &str) -> Error {
Error::partner_mdn(disposition, "partner refused")
}
#[doc(hidden)]
pub fn backend() -> Error {
Error::backend("backend failed", "down".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ErrorContext;
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
#[test]
fn error_is_send_sync_static() {
assert_send_sync_static::<Error>();
assert_send_sync_static::<ErrorKind>();
}
#[test]
fn display_has_no_trailing_period() {
let err = Error::config("partner endpoint must be https");
let text = err.to_string();
assert!(!text.ends_with('.'), "{text}");
assert!(text.starts_with("configuration error: "), "{text}");
}
#[test]
fn source_is_preserved() {
let internal = AsxError::new(
ErrorCode::TransportFailure,
"connection refused",
ErrorContext::new("unit"),
);
let err = Error::from_internal(Stage::HttpNotWritten, internal);
assert_eq!(err.kind(), ErrorKind::Transport);
let source = std::error::Error::source(&err).expect("source kept");
assert!(source.to_string().contains("connection refused"));
}
#[test]
fn partner_retry_rule() {
assert!(Error::partner_ebms("EBMS:0202", "x").is_retryable());
assert!(!Error::partner_ebms("EBMS:0004", "x").is_retryable());
assert!(
Error::partner_mdn("processed/error: unexpected-processing-error", "x").is_retryable()
);
assert!(!Error::partner_mdn("processed/error: decryption-failed", "x").is_retryable());
}
}