Skip to main content

liminal_bus_protocol/
error.rs

1use super::{FrameType, lifecycle::ConnectionState};
2
3/// An expired pass, wherever presented on the wire.
4pub const PASS_EXPIRED_CODE: u16 = 0x000A;
5
6/// Protocol-level failures with stable numeric reason codes for error frames and metrics.
7#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
8pub enum ProtocolError {
9    /// A frame header did not contain all fixed-size header bytes.
10    #[error("incomplete frame header")]
11    IncompleteHeader { message: Option<String> },
12
13    /// A frame header declared more payload bytes than were present.
14    #[error("truncated frame payload")]
15    TruncatedPayload { message: Option<String> },
16
17    /// An unknown frame type was observed for logging or metrics.
18    #[error("unknown frame type {type_id}")]
19    UnknownFrameType {
20        type_id: u8,
21        message: Option<String>,
22    },
23
24    /// A frame appeared on a stream that is invalid for its type.
25    #[error("invalid stream {stream_id} for {frame_type:?}")]
26    InvalidStream {
27        frame_type: FrameType,
28        stream_id: u32,
29        message: Option<String>,
30    },
31
32    /// A frame would move the protocol connection through an invalid state transition.
33    #[error("invalid protocol state transition from {current_state:?} with {frame_type:?}")]
34    InvalidStateTransition {
35        current_state: ConnectionState,
36        frame_type: FrameType,
37        message: Option<String>,
38    },
39
40    /// Authentication failed during connection setup.
41    #[error("authentication failure")]
42    AuthenticationFailure { message: Option<String> },
43
44    /// The presented pass has expired.
45    #[error("pass expired")]
46    PassExpired { message: Option<String> },
47
48    /// The peers could not agree on a compatible protocol version.
49    #[error("protocol version mismatch")]
50    VersionMismatch { message: Option<String> },
51
52    /// A requested schema is incompatible with this connection or stream.
53    #[error("schema incompatible")]
54    SchemaIncompatible { message: Option<String> },
55
56    /// Bytes could not be encoded or decoded according to the frame format.
57    #[error("protocol codec error")]
58    CodecError { message: Option<String> },
59}
60
61impl ProtocolError {
62    /// Malformed header reason code.
63    pub const INCOMPLETE_HEADER_CODE: u16 = 0x0001;
64    /// Truncated payload reason code.
65    pub const TRUNCATED_PAYLOAD_CODE: u16 = 0x0002;
66    /// Unknown frame type reason code.
67    pub const UNKNOWN_FRAME_TYPE_CODE: u16 = 0x0003;
68    /// Invalid stream reason code.
69    pub const INVALID_STREAM_CODE: u16 = 0x0004;
70    /// Invalid state transition reason code.
71    pub const INVALID_STATE_TRANSITION_CODE: u16 = 0x0005;
72    /// Authentication failure reason code.
73    pub const AUTHENTICATION_FAILURE_CODE: u16 = 0x0006;
74    /// Version mismatch reason code.
75    pub const VERSION_MISMATCH_CODE: u16 = 0x0007;
76    /// Schema incompatibility reason code.
77    pub const SCHEMA_INCOMPATIBLE_CODE: u16 = 0x0008;
78    /// Codec error reason code.
79    pub const CODEC_ERROR_CODE: u16 = 0x0009;
80
81    /// Expired pass reason code.
82    pub const PASS_EXPIRED_CODE: u16 = crate::error::PASS_EXPIRED_CODE;
83
84    /// Return the stable numeric reason code for this error.
85    #[must_use]
86    pub const fn reason_code(&self) -> u16 {
87        match self {
88            Self::IncompleteHeader { .. } => Self::INCOMPLETE_HEADER_CODE,
89            Self::TruncatedPayload { .. } => Self::TRUNCATED_PAYLOAD_CODE,
90            Self::UnknownFrameType { .. } => Self::UNKNOWN_FRAME_TYPE_CODE,
91            Self::InvalidStream { .. } => Self::INVALID_STREAM_CODE,
92            Self::InvalidStateTransition { .. } => Self::INVALID_STATE_TRANSITION_CODE,
93            Self::AuthenticationFailure { .. } => Self::AUTHENTICATION_FAILURE_CODE,
94            Self::PassExpired { .. } => PASS_EXPIRED_CODE,
95            Self::VersionMismatch { .. } => Self::VERSION_MISMATCH_CODE,
96            Self::SchemaIncompatible { .. } => Self::SCHEMA_INCOMPATIBLE_CODE,
97            Self::CodecError { .. } => Self::CODEC_ERROR_CODE,
98        }
99    }
100
101    /// Return the optional human-readable message carried by this error.
102    #[must_use]
103    pub fn message(&self) -> Option<&str> {
104        match self {
105            Self::IncompleteHeader { message }
106            | Self::TruncatedPayload { message }
107            | Self::UnknownFrameType { message, .. }
108            | Self::InvalidStream { message, .. }
109            | Self::InvalidStateTransition { message, .. }
110            | Self::AuthenticationFailure { message }
111            | Self::PassExpired { message }
112            | Self::VersionMismatch { message }
113            | Self::SchemaIncompatible { message }
114            | Self::CodecError { message } => message.as_deref(),
115        }
116    }
117
118    #[must_use]
119    pub(crate) fn invalid_stream(frame_type: FrameType, stream_id: u32) -> Self {
120        Self::InvalidStream {
121            frame_type,
122            stream_id,
123            message: Some("stream id violates frame type invariants".to_owned()),
124        }
125    }
126
127    #[must_use]
128    pub(crate) fn invalid_state_transition(
129        current_state: ConnectionState,
130        frame_type: FrameType,
131    ) -> Self {
132        Self::InvalidStateTransition {
133            current_state,
134            frame_type,
135            message: Some("frame is invalid for the current connection state".to_owned()),
136        }
137    }
138
139    #[must_use]
140    pub(crate) fn codec(message: impl Into<String>) -> Self {
141        Self::CodecError {
142            message: Some(message.into()),
143        }
144    }
145}
146
147#[cfg(test)]
148#[path = "pass_expired_tests.rs"]
149mod pass_expired_tests;