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}