Skip to main content

offline_protocol_transport/
error.rs

1//! Transport layer errors.
2
3use thiserror::Error;
4
5/// Result type alias using the transport Error type.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur in the transport layer.
9///
10/// `Display` (the `#[error(...)]` text) is human-facing and free to change.
11/// [`Error::code`] is the stable, machine-readable contract — downstream
12/// telemetry classifies on the code, never on the `Display` wording.
13// Adding a variant to a public error enum is a breaking change without
14// this attribute; downstream crates must carry a wildcard arm.
15#[non_exhaustive]
16#[derive(Debug, Error, Clone, PartialEq, Eq)]
17pub enum Error {
18    /// Transport is not available or supported.
19    #[error("Transport not available: {0}")]
20    TransportNotAvailable(String),
21
22    /// Recipient is not reachable via this transport.
23    /// Unlike `TransportNotAvailable`, the transport itself is healthy --
24    /// only this specific peer cannot be reached through it.
25    #[error("Peer not reachable: {0}")]
26    PeerNotReachable(String),
27
28    /// Failed to send message.
29    #[error("Send failed: {0}")]
30    SendFailed(String),
31
32    /// Failed to receive message.
33    #[error("Receive failed: {0}")]
34    ReceiveFailed(String),
35
36    /// Transport configuration error.
37    #[error("Configuration error: {0}")]
38    ConfigurationError(String),
39
40    /// Serialization/deserialization error.
41    #[error("Serialization error: {0}")]
42    SerializationError(String),
43
44    /// Core error propagated from offline-protocol-core.
45    #[error("Core error: {0}")]
46    Core(#[from] offline_protocol_core::Error),
47
48    /// Message exceeds the configured maximum size.
49    #[error("Message too large: {0} bytes exceeds limit of {1} bytes")]
50    MessageTooLarge(usize, usize),
51
52    /// Cryptographic operation failed (key derivation, signing).
53    #[error("Crypto error: {0}")]
54    CryptoError(String),
55
56    /// Generic error.
57    #[error("{0}")]
58    Other(String),
59}
60
61impl Error {
62    /// Returns a stable, non-localized machine-readable error code.
63    ///
64    /// Unlike [`Display`](std::fmt::Display), this value is a contract:
65    /// downstream consumers (e.g. telemetry classifiers) switch on it instead
66    /// of substring-matching human-readable error text. Keep these strings
67    /// stable — changing one reclassifies every event that carries it.
68    ///
69    /// The match is intentionally exhaustive (no wildcard arm) so that adding
70    /// a new variant fails to compile until it is assigned a code.
71    pub fn code(&self) -> &'static str {
72        match self {
73            Self::TransportNotAvailable(_) => "TRANSPORT_NOT_AVAILABLE",
74            Self::PeerNotReachable(_) => "PEER_NOT_REACHABLE",
75            Self::SendFailed(_) => "SEND_FAILED",
76            Self::ReceiveFailed(_) => "RECEIVE_FAILED",
77            Self::ConfigurationError(_) => "CONFIGURATION_ERROR",
78            Self::SerializationError(_) => "SERIALIZATION_ERROR",
79            Self::Core(_) => "CORE_ERROR",
80            Self::MessageTooLarge(_, _) => "MESSAGE_TOO_LARGE",
81            Self::CryptoError(_) => "CRYPTO_ERROR",
82            Self::Other(_) => "OTHER",
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::Error;
90
91    #[test]
92    fn transport_error_code_values_are_stable() {
93        assert_eq!(
94            Error::TransportNotAvailable("ble".into()).code(),
95            "TRANSPORT_NOT_AVAILABLE"
96        );
97        assert_eq!(
98            Error::PeerNotReachable("peer".into()).code(),
99            "PEER_NOT_REACHABLE"
100        );
101        assert_eq!(Error::SendFailed("boom".into()).code(), "SEND_FAILED");
102        assert_eq!(Error::ReceiveFailed("boom".into()).code(), "RECEIVE_FAILED");
103        assert_eq!(
104            Error::ConfigurationError("bad".into()).code(),
105            "CONFIGURATION_ERROR"
106        );
107        assert_eq!(
108            Error::SerializationError("bad".into()).code(),
109            "SERIALIZATION_ERROR"
110        );
111        assert_eq!(
112            Error::Core(offline_protocol_core::Error::Other("x".into())).code(),
113            "CORE_ERROR"
114        );
115        assert_eq!(
116            Error::MessageTooLarge(2048, 1024).code(),
117            "MESSAGE_TOO_LARGE"
118        );
119        assert_eq!(Error::CryptoError("kdf".into()).code(), "CRYPTO_ERROR");
120        assert_eq!(Error::Other("opaque".into()).code(), "OTHER");
121    }
122
123    #[test]
124    fn transport_error_code_is_independent_of_display() {
125        // The code must not be derived from the Display text.
126        let err = Error::SendFailed("transient radio failure".into());
127        assert_eq!(err.code(), "SEND_FAILED");
128        assert!(err.to_string().contains("transient radio failure"));
129    }
130}