Skip to main content

axon/session_runtime/
error.rs

1//! Operational protocol errors raised by the §Fase 41.d session-typed
2//! WebSocket runtime — every variant is a runtime witness of a static
3//! discipline that the connection must respect on every transition.
4//!
5//! When the static type checker (`axon-frontend`, §41.b/c) has already
6//! validated the bound `session` + `socket { credit }`, a [`ProtocolError`]
7//! at runtime can only fire because the **peer** sent a frame that diverges
8//! from the conformant trace (or because a malformed frame entered the
9//! decoder). The carrier (WebSocket) closes with code `1002 protocol error`
10//! when one of these is observed; the error is recorded verbatim in the
11//! close-reason payload so the peer can diagnose the divergence.
12
13use std::fmt;
14
15use axon_frontend::session::Payload;
16
17/// Runtime protocol violation — the peer's next frame is inconsistent with
18/// the session-type cursor or with the credit window.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ProtocolError {
21    /// The cursor expected a `send`/`recv` of payload `expected` but the
22    /// peer's frame announced payload `got`. The static type discipline is
23    /// violated — at runtime this means the peer is not running the dual
24    /// of our declared role.
25    PayloadMismatch { expected: Payload, got: Payload },
26    /// The cursor expected an operation of one kind (e.g. `recv`) but the
27    /// peer's frame announced another (e.g. `select`). The connection state
28    /// machine has no transition rule for the observed input.
29    UnexpectedFrame {
30        cursor_kind: &'static str,
31        frame_kind: &'static str,
32    },
33    /// The cursor is at an internal/external choice and the peer's label is
34    /// not in the type's arm set. Lists the declared labels so the peer can
35    /// recover.
36    UnknownLabel { label: String, expected: Vec<String> },
37    /// A `send` was attempted at zero available credit — the §Fase 41.c
38    /// "no rule at `n = 0`" axiom (paper §4.2) projected onto the runtime.
39    /// Static analysis (`credit_analyse`) catches this at compile time
40    /// when the declared protocol demands more than `k` sends in a burst;
41    /// at runtime it is the dynamic-safety net for an off-spec peer.
42    CreditExhausted { payload: Payload, budget: u64 },
43    /// The cursor has reached `end` but the peer sent more data, or the
44    /// peer requested an action while we already closed our half.
45    AlreadyComplete { frame_kind: &'static str },
46    /// The frame did not parse as a well-formed AXON session-typed
47    /// envelope (malformed JSON, unknown `kind`, missing required field).
48    /// Carries the raw payload for diagnostics.
49    MalformedFrame(String),
50    /// The transport (WebSocket) returned an I/O error or was closed
51    /// abruptly mid-dialogue.
52    Transport(String),
53}
54
55impl fmt::Display for ProtocolError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            ProtocolError::PayloadMismatch { expected, got } => write!(
59                f,
60                "payload mismatch: cursor expected `{expected}`, peer sent `{got}` \
61                 (the connection is not dual to the declared role)"
62            ),
63            ProtocolError::UnexpectedFrame { cursor_kind, frame_kind } => write!(
64                f,
65                "unexpected frame: cursor is at `{cursor_kind}`, peer sent `{frame_kind}` \
66                 — the state machine has no transition for this input"
67            ),
68            ProtocolError::UnknownLabel { label, expected } => write!(
69                f,
70                "unknown choice label `{label}` — declared labels: {}",
71                expected.join(", ")
72            ),
73            ProtocolError::CreditExhausted { payload, budget } => write!(
74                f,
75                "credit exhausted on `send {payload}` at window n = 0 \
76                 (budget = {budget}, §Fase 41.c, paper §4.2)"
77            ),
78            ProtocolError::AlreadyComplete { frame_kind } => write!(
79                f,
80                "dialogue already at `end`; peer sent `{frame_kind}` post-termination"
81            ),
82            ProtocolError::MalformedFrame(detail) => write!(f, "malformed frame: {detail}"),
83            ProtocolError::Transport(detail) => write!(f, "transport error: {detail}"),
84        }
85    }
86}
87
88impl std::error::Error for ProtocolError {}
89
90impl ProtocolError {
91    /// A compact identifier suitable for the WebSocket close-frame reason
92    /// payload (RFC 6455 §5.5.1 caps the reason at 123 bytes UTF-8 — keep
93    /// these stable, short and machine-readable).
94    pub fn code(&self) -> &'static str {
95        match self {
96            ProtocolError::PayloadMismatch { .. } => "payload_mismatch",
97            ProtocolError::UnexpectedFrame { .. } => "unexpected_frame",
98            ProtocolError::UnknownLabel { .. } => "unknown_label",
99            ProtocolError::CreditExhausted { .. } => "credit_exhausted",
100            ProtocolError::AlreadyComplete { .. } => "already_complete",
101            ProtocolError::MalformedFrame(_) => "malformed_frame",
102            ProtocolError::Transport(_) => "transport",
103        }
104    }
105}