Skip to main content

protocol/
error.rs

1//! Structured errors (spec: Structured errors): a stable snake_case code, a
2//! human-readable message, and whether retrying the same request is meaningful.
3
4use core::fmt;
5
6use minicbor::{Decode, Encode};
7
8use crate::capability::Capabilities;
9use crate::message::Method;
10
11/// An error carried in a response or welcome, or produced by the host
12/// session on the agent's behalf (a missing capability, a passed deadline,
13/// a lost connection).
14///
15/// On the wire: `{0: code, 1: message, 2: retryable}`.
16#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
17#[cbor(map)]
18pub struct Error {
19    /// Stable snake_case code; see [`code`].
20    #[n(0)]
21    pub code: String,
22    /// Human-readable description, including any hint for the user.
23    #[n(1)]
24    pub message: String,
25    /// Whether sending the same request again can succeed.
26    #[n(2)]
27    pub retryable: bool,
28}
29
30/// The error codes this protocol version defines. Agents may send others;
31/// [`retryable`] returns `None` for those.
32pub mod code {
33    /// The agent does not implement the method. Not retryable.
34    pub const UNKNOWN_METHOD: &str = "unknown_method";
35    /// The parameters are missing or malformed. Not retryable.
36    pub const INVALID_PARAMS: &str = "invalid_params";
37    /// The method needs a capability the agent did not advertise; the host
38    /// refused to send it. Not retryable.
39    pub const CAPABILITY_MISSING: &str = "capability_missing";
40    /// The request's deadline passed before the work finished. Retryable: the
41    /// agent stays responsive and the same request can finish another time.
42    pub const DEADLINE_EXCEEDED: &str = "deadline_exceeded";
43    /// The agent is still finishing an abandoned request. Retryable.
44    pub const BUSY: &str = "busy";
45    /// The foreground window changed during acquisition. Retryable.
46    pub const WINDOW_CHANGED: &str = "window_changed";
47    /// The message exceeds the negotiated maximum frame size. Not retryable.
48    pub const FRAME_TOO_LARGE: &str = "frame_too_large";
49    /// The two sides share no protocol version. Not retryable.
50    pub const VERSION_MISMATCH: &str = "version_mismatch";
51    /// The agent's hello carried the wrong per-launch token. Not retryable.
52    pub const TOKEN_MISMATCH: &str = "token_mismatch";
53    /// The negotiated maximum frame size is below the protocol minimum. Not
54    /// retryable.
55    pub const FRAME_LIMIT_TOO_SMALL: &str = "frame_limit_too_small";
56    /// The handshake has not completed yet. Retryable once it has.
57    pub const NOT_READY: &str = "not_ready";
58    /// The connection to the agent is gone. Not retryable on this connection;
59    /// the message carries a reconnect hint.
60    pub const DISCONNECTED: &str = "disconnected";
61    /// A malformed or unexpected message. Not retryable.
62    pub const PROTOCOL_ERROR: &str = "protocol_error";
63    /// An unexpected failure inside the agent. Not retryable.
64    pub const INTERNAL: &str = "internal";
65
66    /// Every code above.
67    pub const ALL: &[&str] = &[
68        UNKNOWN_METHOD,
69        INVALID_PARAMS,
70        CAPABILITY_MISSING,
71        DEADLINE_EXCEEDED,
72        BUSY,
73        WINDOW_CHANGED,
74        FRAME_TOO_LARGE,
75        VERSION_MISMATCH,
76        TOKEN_MISMATCH,
77        FRAME_LIMIT_TOO_SMALL,
78        NOT_READY,
79        DISCONNECTED,
80        PROTOCOL_ERROR,
81        INTERNAL,
82    ];
83}
84
85/// Whether retrying is meaningful for a code this protocol version defines,
86/// or `None` for any other code.
87pub fn retryable(code: &str) -> Option<bool> {
88    match code {
89        code::DEADLINE_EXCEEDED | code::BUSY | code::WINDOW_CHANGED | code::NOT_READY => Some(true),
90        code::UNKNOWN_METHOD
91        | code::INVALID_PARAMS
92        | code::CAPABILITY_MISSING
93        | code::FRAME_TOO_LARGE
94        | code::VERSION_MISMATCH
95        | code::TOKEN_MISMATCH
96        | code::FRAME_LIMIT_TOO_SMALL
97        | code::DISCONNECTED
98        | code::PROTOCOL_ERROR
99        | code::INTERNAL => Some(false),
100        _ => None,
101    }
102}
103
104impl Error {
105    /// An error with `code`, retryable as [`retryable`] says (not retryable
106    /// for codes this version does not define).
107    pub fn new(code: &str, message: impl Into<String>) -> Error {
108        Error {
109            code: code.to_owned(),
110            message: message.into(),
111            retryable: retryable(code).unwrap_or(false),
112        }
113    }
114
115    /// The agent does not implement `method`.
116    pub fn unknown_method(method: Method) -> Error {
117        Error::new(
118            code::UNKNOWN_METHOD,
119            format!("{method} is not a method this agent implements"),
120        )
121    }
122
123    /// `method` needs `missing`, which the agent did not advertise.
124    pub fn capability_missing(method: Method, missing: Capabilities) -> Error {
125        Error::new(
126            code::CAPABILITY_MISSING,
127            format!(
128                "the agent did not advertise {} ({missing}), which {method} needs",
129                missing.label()
130            ),
131        )
132    }
133
134    /// Request `id` for `method` did not finish within `deadline_ms`.
135    pub fn deadline_exceeded(method: Method, id: u32, deadline_ms: u32) -> Error {
136        Error::new(
137            code::DEADLINE_EXCEEDED,
138            format!(
139                "the {method} request {id} did not finish within its {deadline_ms} ms deadline"
140            ),
141        )
142    }
143
144    /// The `method` message for request `id` is `size` bytes, over `limit`.
145    pub fn frame_too_large(method: Method, id: u32, size: u64, limit: u32) -> Error {
146        Error::new(
147            code::FRAME_TOO_LARGE,
148            format!(
149                "the {method} message for request {id} is {size} bytes, over the negotiated maximum frame size of {limit} bytes"
150            ),
151        )
152    }
153
154    /// The connection is gone because of `reason`; carries the reconnect hint.
155    pub fn disconnected(reason: impl fmt::Display) -> Error {
156        Error::new(
157            code::DISCONNECTED,
158            format!(
159                "the agent is disconnected ({reason}); reconnect to the target, which restarts or redeploys the agent"
160            ),
161        )
162    }
163}
164
165impl fmt::Display for Error {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(f, "{}: {}", self.code, self.message)
168    }
169}
170
171impl std::error::Error for Error {}