use std::error::Error;
use std::fmt;
use crate::bus::{PayloadDecodeError, TransportError, TransportErrorKind};
use crate::lock::RetryClass;
use crate::{repository::RepositoryError, EventRecordError};
#[derive(Debug)]
#[non_exhaustive]
pub enum HandlerError {
UnknownCommand(String),
DecodeFailed(String),
Rejected(String),
NotFound(String),
Unauthorized(String),
Repository(RepositoryError),
GuardRejected(String),
Other(Box<dyn Error + Send + Sync>),
}
impl fmt::Display for HandlerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HandlerError::UnknownCommand(name) => write!(f, "unknown command: {}", name),
HandlerError::DecodeFailed(msg) => write!(f, "decode failed: {}", msg),
HandlerError::Rejected(msg) => write!(f, "rejected: {}", msg),
HandlerError::NotFound(id) => write!(f, "not found: {}", id),
HandlerError::Unauthorized(msg) => write!(f, "unauthorized: {}", msg),
HandlerError::Repository(e) => write!(f, "repository error: {}", e),
HandlerError::GuardRejected(name) => {
write!(f, "guard rejected command: {}", name)
}
HandlerError::Other(e) => write!(f, "handler error: {}", e),
}
}
}
impl Error for HandlerError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
HandlerError::Repository(e) => Some(e),
HandlerError::Other(e) => Some(e.as_ref()),
_ => None,
}
}
}
impl From<RepositoryError> for HandlerError {
fn from(err: RepositoryError) -> Self {
HandlerError::Repository(err)
}
}
impl From<EventRecordError> for HandlerError {
fn from(err: EventRecordError) -> Self {
HandlerError::Other(Box::new(err))
}
}
impl From<serde_json::Error> for HandlerError {
fn from(err: serde_json::Error) -> Self {
HandlerError::DecodeFailed(err.to_string())
}
}
impl From<PayloadDecodeError> for HandlerError {
fn from(err: PayloadDecodeError) -> Self {
HandlerError::DecodeFailed(err.0)
}
}
impl HandlerError {
pub fn status_code(&self) -> u16 {
match self {
HandlerError::UnknownCommand(_) => 404,
HandlerError::DecodeFailed(_) => 400,
HandlerError::Rejected(_) => 422,
HandlerError::NotFound(_) => 404,
HandlerError::Unauthorized(_) => 401,
HandlerError::Repository(_) => 500,
HandlerError::GuardRejected(_) => 400,
HandlerError::Other(_) => 500,
}
}
pub fn client_facing_message(&self) -> String {
if self.status_code() >= 500 {
"Internal server error".to_string()
} else {
self.to_string()
}
}
pub(crate) fn transport_error_kind(&self) -> TransportErrorKind {
match self {
HandlerError::Repository(err) => match err.kind() {
RetryClass::Retryable => TransportErrorKind::Retryable,
RetryClass::Permanent => TransportErrorKind::Permanent,
},
HandlerError::NotFound(_) | HandlerError::Other(_) => TransportErrorKind::Retryable,
HandlerError::UnknownCommand(_)
| HandlerError::DecodeFailed(_)
| HandlerError::Rejected(_)
| HandlerError::Unauthorized(_)
| HandlerError::GuardRejected(_) => TransportErrorKind::Permanent,
}
}
}
impl From<HandlerError> for TransportError {
fn from(error: HandlerError) -> Self {
let kind = error.transport_error_kind();
TransportError::new(kind, error.to_string()).with_source(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transient_handler_errors_are_retryable() {
for error in [
HandlerError::Repository(RepositoryError::NotFound { id: "agg-1".into() }),
HandlerError::NotFound("agg-1".into()),
HandlerError::Other("boom".into()),
] {
assert_eq!(error.transport_error_kind(), TransportErrorKind::Retryable);
}
}
#[test]
fn deterministic_handler_errors_are_permanent() {
for error in [
HandlerError::UnknownCommand("x".into()),
HandlerError::DecodeFailed("x".into()),
HandlerError::Rejected("x".into()),
HandlerError::Unauthorized("x".into()),
HandlerError::GuardRejected("x".into()),
] {
assert_eq!(error.transport_error_kind(), TransportErrorKind::Permanent);
}
}
#[test]
fn deterministic_repository_errors_are_permanent() {
let io = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad row");
for error in [
HandlerError::Repository(RepositoryError::Model("invalid model state".into())),
HandlerError::Repository(RepositoryError::Replay("version mismatch".into())),
HandlerError::Repository(RepositoryError::permanent_storage("insert event", io)),
] {
assert_eq!(
error.transport_error_kind(),
TransportErrorKind::Permanent,
"{error}"
);
}
}
#[test]
fn transient_repository_storage_errors_are_retryable() {
let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
let error = HandlerError::Repository(RepositoryError::retryable_storage("load stream", io));
assert_eq!(error.transport_error_kind(), TransportErrorKind::Retryable);
let conflict = HandlerError::Repository(RepositoryError::ConcurrentWrite {
id: "agg-1".into(),
expected: 1,
actual: 2,
});
assert_eq!(
conflict.transport_error_kind(),
TransportErrorKind::Retryable
);
}
#[test]
fn from_handler_error_preserves_classification_and_source() {
let err: TransportError = HandlerError::Rejected("invalid".into()).into();
assert!(err.is_permanent());
assert!(err.source().is_some());
let err: TransportError =
HandlerError::Other(Box::<dyn Error + Send + Sync>::from("infra")).into();
assert!(err.is_retryable());
}
}