Skip to main content

hexeract_outbox/
error.rs

1use thiserror::Error;
2use uuid::Uuid;
3
4/// Errors raised by the outbox primitives, publishers and workers.
5///
6/// Marked `#[non_exhaustive]` so new variants can be added without a
7/// breaking change.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum OutboxError {
11    /// The event payload could not be serialized or deserialized as JSON.
12    #[error("failed to (de)serialize event payload as JSON")]
13    Serialization(#[from] serde_json::Error),
14
15    /// The backend reported a database-level failure.
16    ///
17    /// The original error is preserved as a boxed source so callers can
18    /// downcast if they need typed access to the underlying driver error.
19    #[error("database error")]
20    Database(#[source] Box<dyn std::error::Error + Send + Sync>),
21
22    /// The worker polled an envelope whose `event_type` has no registered handler.
23    #[error("no handler registered for event type `{event_type}`")]
24    MissingHandler {
25        /// The unrouted event type read from the envelope.
26        event_type: String,
27    },
28
29    /// The envelope was retried more times than the configured maximum.
30    #[error("event {event_id} reached max retries after {attempts} attempts")]
31    MaxRetries {
32        /// Identifier of the event that exhausted its retry budget.
33        event_id: Uuid,
34        /// Number of attempts already consumed.
35        attempts: u32,
36    },
37
38    /// An envelope was decoded into the wrong event type.
39    ///
40    /// Returned when a caller invokes [`crate::OutboxEnvelope::decode`]
41    /// with a type whose [`crate::Event::EVENT_TYPE`] does not match the
42    /// envelope's `event_type` field. Typically the sign of a
43    /// router or registry misconfiguration on the caller side.
44    #[error("envelope carries event_type `{actual}` but decode requested `{expected}`")]
45    TypeMismatch {
46        /// Event type requested by the caller (`E::EVENT_TYPE`).
47        expected: &'static str,
48        /// Event type actually stored in the envelope.
49        actual: String,
50    },
51
52    /// An invariant of the outbox machinery was violated.
53    ///
54    /// Signals a bug in the framework itself, not a recoverable error.
55    /// Report occurrences upstream.
56    #[error("internal outbox error: {0}")]
57    Internal(String),
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn serialization_error_is_built_from_serde_json() {
66        let invalid_json = b"not json";
67        let serde_error: serde_json::Error =
68            serde_json::from_slice::<serde_json::Value>(invalid_json).unwrap_err();
69        let error: OutboxError = serde_error.into();
70        assert!(matches!(error, OutboxError::Serialization(_)));
71    }
72
73    #[test]
74    fn database_error_preserves_source_chain() {
75        let inner = std::io::Error::other("disk on fire");
76        let error = OutboxError::Database(Box::new(inner));
77        let source = std::error::Error::source(&error).expect("source must be set");
78        assert_eq!(source.to_string(), "disk on fire");
79    }
80
81    #[test]
82    fn missing_handler_message_includes_event_type() {
83        let error = OutboxError::MissingHandler {
84            event_type: "users.registered".to_owned(),
85        };
86        assert!(error.to_string().contains("users.registered"));
87    }
88
89    #[test]
90    fn max_retries_message_includes_event_id_and_count() {
91        let event_id = Uuid::from_u128(7);
92        let error = OutboxError::MaxRetries {
93            event_id,
94            attempts: 5,
95        };
96        let message = error.to_string();
97        assert!(message.contains(&event_id.to_string()));
98        assert!(message.contains('5'));
99    }
100}