Skip to main content

liminal/protocol/
error.rs

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