Skip to main content

secretspec_ipc/
error.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use thiserror::Error;
4
5/// The provider-owned interaction that must finish before an operation can be
6/// retried.
7///
8/// The set is closed for senders and open for receivers for the same reason as
9/// [`ErrorKind`]: adding another interaction kind must not make an older client
10/// reject an otherwise well-formed error response.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InteractionKind {
13    Authorization,
14    Unrecognized,
15}
16
17impl InteractionKind {
18    pub const fn as_str(self) -> &'static str {
19        match self {
20            Self::Authorization => "authorization",
21            Self::Unrecognized => "unrecognized",
22        }
23    }
24
25    pub fn from_wire(kind: &str) -> Self {
26        match kind {
27            "authorization" => Self::Authorization,
28            _ => Self::Unrecognized,
29        }
30    }
31}
32
33impl Serialize for InteractionKind {
34    fn serialize<S: serde::Serializer>(
35        &self,
36        serializer: S,
37    ) -> std::result::Result<S::Ok, S::Error> {
38        serializer.serialize_str(self.as_str())
39    }
40}
41
42impl<'de> Deserialize<'de> for InteractionKind {
43    fn deserialize<D: serde::Deserializer<'de>>(
44        deserializer: D,
45    ) -> std::result::Result<Self, D::Error> {
46        Ok(Self::from_wire(&String::deserialize(deserializer)?))
47    }
48}
49
50/// Opaque, non-secret correlation data for provider-owned interaction.
51/// Available starting with SecretSpec 0.21.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct InteractionReference {
55    pub kind: InteractionKind,
56    pub id: String,
57    #[serde(deserialize_with = "crate::protocol::deserialize_required_nullable")]
58    pub expires_at_unix_ms: Option<u64>,
59}
60
61impl InteractionReference {
62    pub fn authorization(id: impl Into<String>, expires_at_unix_ms: Option<u64>) -> Self {
63        Self {
64            kind: InteractionKind::Authorization,
65            id: id.into(),
66            expires_at_unix_ms,
67        }
68    }
69
70    fn validate(&self) -> std::result::Result<(), &'static str> {
71        if self.id.is_empty()
72            || self.id.len() > 128
73            || !self
74                .id
75                .bytes()
76                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
77        {
78            return Err("interaction id has an invalid format");
79        }
80        if self.expires_at_unix_ms == Some(0) {
81            return Err("interaction expiry must be positive");
82        }
83        Ok(())
84    }
85}
86
87/// Stable machine-readable error kinds shared by both application protocols.
88///
89/// The set is closed for senders and open for receivers. An endpoint emits only
90/// the kinds below, but a receiver decodes anything it does not recognize as
91/// [`ErrorKind::Unrecognized`] instead of failing the frame. Without that, the
92/// first kind a later protocol version adds would kill every session with an
93/// older peer, and the set could never grow without a new protocol version.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum ErrorKind {
96    ParseError,
97    InvalidRequest,
98    MethodNotFound,
99    InvalidParams,
100    Internal,
101    UnsupportedVersion,
102    CapabilityRequired,
103    DeadlineExceeded,
104    Cancelled,
105    Unavailable,
106    PermissionDenied,
107    InteractionRequired,
108    Conflict,
109    OperationFailed,
110    MessageTooLarge,
111    RepresentationMismatch,
112    /// A kind this implementation does not know, decoded from a peer that
113    /// speaks a later revision of the protocol.
114    ///
115    /// Receiving one means the operation failed for a reason this side cannot
116    /// name, so it is handled as a failure and never as a success. An endpoint
117    /// MUST NOT construct it: doing so would put `"unrecognized"` on the wire
118    /// as if it were a defined kind.
119    Unrecognized,
120}
121
122impl ErrorKind {
123    /// Stable wire spelling used for structured handling and redacted audit
124    /// records.
125    pub const fn as_str(self) -> &'static str {
126        match self {
127            Self::ParseError => "parse_error",
128            Self::InvalidRequest => "invalid_request",
129            Self::MethodNotFound => "method_not_found",
130            Self::InvalidParams => "invalid_params",
131            Self::Internal => "internal",
132            Self::UnsupportedVersion => "unsupported_version",
133            Self::CapabilityRequired => "capability_required",
134            Self::DeadlineExceeded => "deadline_exceeded",
135            Self::Cancelled => "cancelled",
136            Self::Unavailable => "unavailable",
137            Self::PermissionDenied => "permission_denied",
138            Self::InteractionRequired => "interaction_required",
139            Self::Conflict => "conflict",
140            Self::OperationFailed => "operation_failed",
141            Self::MessageTooLarge => "message_too_large",
142            Self::RepresentationMismatch => "representation_mismatch",
143            Self::Unrecognized => "unrecognized",
144        }
145    }
146
147    /// Decode a wire spelling, mapping anything unknown to
148    /// [`Self::Unrecognized`] rather than refusing the frame.
149    pub fn from_wire(kind: &str) -> Self {
150        Self::DEFINED
151            .iter()
152            .copied()
153            .find(|defined| defined.as_str() == kind)
154            .unwrap_or(Self::Unrecognized)
155    }
156
157    /// The kind a defined code names, or `None` for a code this revision does
158    /// not define. Used to keep a peer honest: a code this side knows must
159    /// arrive with the kind that belongs to it, and only a code it does not
160    /// know may carry a kind it does not know.
161    pub fn from_code(code: i32) -> Option<Self> {
162        Self::DEFINED
163            .iter()
164            .copied()
165            .find(|defined| defined.code() == code)
166    }
167
168    /// Every kind this revision defines, in wire-code order. `Unrecognized` is
169    /// deliberately absent: it is a decoding outcome, not a defined kind.
170    pub const DEFINED: &'static [Self] = &[
171        Self::ParseError,
172        Self::InvalidRequest,
173        Self::MethodNotFound,
174        Self::InvalidParams,
175        Self::Internal,
176        Self::UnsupportedVersion,
177        Self::CapabilityRequired,
178        Self::DeadlineExceeded,
179        Self::Cancelled,
180        Self::Unavailable,
181        Self::PermissionDenied,
182        Self::InteractionRequired,
183        Self::Conflict,
184        Self::OperationFailed,
185        Self::MessageTooLarge,
186        Self::RepresentationMismatch,
187    ];
188
189    pub const fn code(self) -> i32 {
190        match self {
191            Self::ParseError => -32700,
192            Self::InvalidRequest => -32600,
193            Self::MethodNotFound => -32601,
194            Self::InvalidParams => -32602,
195            Self::Internal => -32603,
196            Self::UnsupportedVersion => -32000,
197            Self::CapabilityRequired => -32001,
198            Self::DeadlineExceeded => -32002,
199            Self::Cancelled => -32003,
200            Self::Unavailable => -32004,
201            Self::PermissionDenied => -32005,
202            Self::InteractionRequired => -32006,
203            Self::Conflict => -32007,
204            Self::OperationFailed => -32008,
205            Self::MessageTooLarge => -32009,
206            Self::RepresentationMismatch => -32010,
207            // Never sent, so it names no code of its own. The wire code that
208            // arrived with it is carried by `RpcError::code`.
209            Self::Unrecognized => 0,
210        }
211    }
212
213    pub const fn message(self) -> &'static str {
214        match self {
215            Self::ParseError => "parse error",
216            Self::InvalidRequest => "invalid request",
217            Self::MethodNotFound => "method not found",
218            Self::InvalidParams => "invalid params",
219            Self::Internal => "internal error",
220            Self::UnsupportedVersion => "unsupported version",
221            Self::CapabilityRequired => "capability required",
222            Self::DeadlineExceeded => "deadline exceeded",
223            Self::Cancelled => "cancelled",
224            Self::Unavailable => "unavailable",
225            Self::PermissionDenied => "permission denied",
226            Self::InteractionRequired => "interaction required",
227            Self::Conflict => "conflict",
228            Self::OperationFailed => "operation failed",
229            Self::MessageTooLarge => "message too large",
230            Self::RepresentationMismatch => "representation mismatch",
231            Self::Unrecognized => "unrecognized error",
232        }
233    }
234
235    pub const fn retryable_by_default(self) -> bool {
236        matches!(self, Self::Unavailable)
237    }
238}
239
240impl fmt::Display for ErrorKind {
241    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242        formatter.write_str(self.message())
243    }
244}
245
246impl Serialize for ErrorKind {
247    fn serialize<S: serde::Serializer>(
248        &self,
249        serializer: S,
250    ) -> std::result::Result<S::Ok, S::Error> {
251        serializer.serialize_str(self.as_str())
252    }
253}
254
255impl<'de> Deserialize<'de> for ErrorKind {
256    fn deserialize<D: serde::Deserializer<'de>>(
257        deserializer: D,
258    ) -> std::result::Result<Self, D::Error> {
259        // Deliberately infallible for any string. Refusing an unknown kind here
260        // would turn every later addition into a session-killing parse failure
261        // against an older peer, which is exactly what the open receiver rule
262        // exists to prevent. `RpcError::validate` still rejects an unknown kind
263        // that arrived with a code this revision does define.
264        Ok(Self::from_wire(&String::deserialize(deserializer)?))
265    }
266}
267
268/// Closed JSON-RPC `error.data` payload.
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct ErrorData {
272    pub kind: ErrorKind,
273    pub retryable: bool,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub retry_after_ms: Option<u64>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub interaction: Option<InteractionReference>,
278}
279
280/// A stable, redacted JSON-RPC error.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(deny_unknown_fields)]
283pub struct RpcError {
284    pub code: i32,
285    pub message: String,
286    pub data: ErrorData,
287}
288
289impl RpcError {
290    pub fn new(kind: ErrorKind) -> Self {
291        Self {
292            code: kind.code(),
293            message: kind.message().to_string(),
294            data: ErrorData {
295                kind,
296                retryable: kind.retryable_by_default(),
297                retry_after_ms: None,
298                interaction: None,
299            },
300        }
301    }
302
303    pub fn interaction_required(reference: Option<InteractionReference>) -> Self {
304        let mut error = Self::new(ErrorKind::InteractionRequired);
305        error.data.interaction = reference;
306        error
307    }
308
309    pub fn unavailable(retry_after_ms: Option<u64>) -> Self {
310        let mut error = Self::new(ErrorKind::Unavailable);
311        error.data.retry_after_ms = retry_after_ms;
312        error
313    }
314
315    pub fn validate(&self) -> std::result::Result<(), &'static str> {
316        if self.data.kind == ErrorKind::Unrecognized {
317            // A code this revision defines must arrive with the kind that
318            // belongs to it. Only a code it has never heard of may carry a kind
319            // it has never heard of, which is what a later revision sends.
320            if ErrorKind::from_code(self.code).is_some() {
321                return Err("defined error code arrived with an undefined kind");
322            }
323        } else if self.code != self.data.kind.code() {
324            return Err("error code does not match error kind");
325        }
326        if self.message.is_empty() || self.message.len() > 256 {
327            return Err("error message has an invalid byte length");
328        }
329        if let Some(retry_after_ms) = self.data.retry_after_ms {
330            // Allowed alongside an unrecognized kind because a later revision
331            // may define another retryable one, and refusing it here would make
332            // that addition unreachable.
333            if !matches!(
334                self.data.kind,
335                ErrorKind::Unavailable | ErrorKind::Unrecognized
336            ) {
337                return Err("retry_after_ms is only valid for unavailable");
338            }
339            if retry_after_ms == 0 {
340                return Err("retry_after_ms must be positive");
341            }
342        }
343        if let Some(interaction) = &self.data.interaction {
344            if self.data.kind != ErrorKind::InteractionRequired {
345                return Err("interaction is only valid for interaction_required");
346            }
347            interaction.validate()?;
348        }
349        Ok(())
350    }
351}
352
353#[derive(Debug, Error)]
354pub enum Error {
355    #[error("I/O error")]
356    Io(#[from] std::io::Error),
357    #[error("protocol error: {0}")]
358    Protocol(&'static str),
359    #[error("protocol error: {0}")]
360    ProtocolOwned(String),
361    #[error("remote {0:?}")]
362    Remote(RpcError),
363    #[error("request cancelled")]
364    Cancelled,
365    #[error("request deadline exceeded")]
366    DeadlineExceeded,
367    #[error("endpoint unavailable")]
368    Unavailable,
369    #[error("session is closed")]
370    Closed,
371}
372
373impl Error {
374    /// The closed RPC kind when the failure came from an accepted request.
375    /// Transport and local protocol failures have no remote application kind.
376    pub const fn rpc_kind(&self) -> Option<ErrorKind> {
377        match self {
378            Self::Remote(error) => Some(error.data.kind),
379            Self::Cancelled => Some(ErrorKind::Cancelled),
380            Self::DeadlineExceeded => Some(ErrorKind::DeadlineExceeded),
381            Self::Unavailable => Some(ErrorKind::Unavailable),
382            Self::Io(_) | Self::Protocol(_) | Self::ProtocolOwned(_) | Self::Closed => None,
383        }
384    }
385
386    /// A non-secret local description suitable for an FFI error object.
387    pub const fn stable_message(&self) -> &'static str {
388        match self {
389            Self::Io(_) => "I/O error",
390            Self::Protocol(_) | Self::ProtocolOwned(_) => "protocol error",
391            Self::Remote(_) => "remote error",
392            Self::Cancelled => "cancelled",
393            Self::DeadlineExceeded => "deadline exceeded",
394            Self::Unavailable => "unavailable",
395            Self::Closed => "session closed",
396        }
397    }
398}
399
400pub type Result<T> = std::result::Result<T, Error>;
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn rpc_kind_preserves_closed_remote_semantics() {
408        for kind in [
409            ErrorKind::InteractionRequired,
410            ErrorKind::PermissionDenied,
411            ErrorKind::Conflict,
412            ErrorKind::Unavailable,
413        ] {
414            let error = if kind == ErrorKind::Unavailable {
415                Error::Unavailable
416            } else {
417                Error::Remote(RpcError::new(kind))
418            };
419            assert_eq!(error.rpc_kind(), Some(kind));
420            assert_eq!(kind.to_string(), kind.message());
421            assert!(!kind.as_str().contains(' '));
422        }
423        assert_eq!(Error::Closed.rpc_kind(), None);
424        assert_eq!(Error::Protocol("bad frame").rpc_kind(), None);
425    }
426
427    /// The rule that keeps the error set growable. A later revision naming a
428    /// kind this one has never heard of must be decodable, or the first
429    /// addition breaks every deployed peer and the set is frozen for the life
430    /// of the protocol version.
431    #[test]
432    fn an_undefined_kind_decodes_instead_of_failing_the_frame() {
433        let later: RpcError = serde_json::from_str(
434            r#"{"code":-32011,"message":"dynamic session required",
435                "data":{"kind":"dynamic_session_required","retryable":false}}"#,
436        )
437        .unwrap();
438        assert_eq!(later.data.kind, ErrorKind::Unrecognized);
439        later.validate().unwrap();
440        // It is a failure, never a success, and never silently retryable.
441        assert!(!later.data.kind.retryable_by_default());
442
443        // A code this revision defines must still arrive with its own kind. A
444        // peer that disagrees there is broken, not merely newer.
445        let mismatched: RpcError = serde_json::from_str(
446            r#"{"code":-32005,"message":"permission denied",
447                "data":{"kind":"something_else","retryable":false}}"#,
448        )
449        .unwrap();
450        assert_eq!(mismatched.data.kind, ErrorKind::Unrecognized);
451        assert!(mismatched.validate().is_err());
452
453        // Every defined kind still round-trips through its wire spelling.
454        for kind in ErrorKind::DEFINED {
455            assert_eq!(ErrorKind::from_wire(kind.as_str()), *kind);
456            assert_eq!(ErrorKind::from_code(kind.code()), Some(*kind));
457        }
458        assert_eq!(ErrorKind::from_code(-32011), None);
459    }
460
461    #[test]
462    fn retry_after_must_be_positive() {
463        assert!(RpcError::unavailable(Some(1)).validate().is_ok());
464        assert!(RpcError::unavailable(Some(0)).validate().is_err());
465    }
466
467    #[test]
468    fn interaction_references_are_bounded_and_kind_specific() {
469        let reference = InteractionReference::authorization("apr_7K3M", Some(1));
470        let error = RpcError::interaction_required(Some(reference.clone()));
471        error.validate().unwrap();
472        let encoded = serde_json::to_string(&error).unwrap();
473        let decoded: RpcError = serde_json::from_str(&encoded).unwrap();
474        assert_eq!(decoded.data.interaction, Some(reference));
475
476        let mut wrong_kind = RpcError::new(ErrorKind::PermissionDenied);
477        wrong_kind.data.interaction = Some(InteractionReference::authorization("apr_1", None));
478        assert!(wrong_kind.validate().is_err());
479
480        assert!(
481            RpcError::interaction_required(Some(InteractionReference::authorization(
482                "terminal escape \u{1b}",
483                None,
484            )))
485            .validate()
486            .is_err()
487        );
488    }
489
490    #[test]
491    fn later_interaction_kinds_decode_without_becoming_authority() {
492        let error: RpcError = serde_json::from_str(
493            r#"{"code":-32006,"message":"interaction required",
494                "data":{"kind":"interaction_required","retryable":false,
495                "interaction":{"kind":"later_kind","id":"ref_1",
496                "expires_at_unix_ms":null}}}"#,
497        )
498        .unwrap();
499        assert_eq!(
500            error.data.interaction.as_ref().unwrap().kind,
501            InteractionKind::Unrecognized
502        );
503        error.validate().unwrap();
504    }
505}