Skip to main content

gbp/
error_object.rs

1//! Wire-serialisable error object.
2
3use crate::CodecError;
4use gbp_core::{ErrorClass, errors::ErrorSpec};
5use serde::{Deserialize, Serialize};
6use serde_bytes::ByteBuf;
7
8/// Wire-serialisable error object.
9///
10/// `details_cbor` carries opaque, structured details. Implementations MUST NOT
11/// place secret material or sensitive payload bytes into `reason` or
12/// `details_cbor` — error objects are forwarded across trust boundaries.
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct ErrorObject {
15    /// Numeric error code (see `gbp_core::errors::codes`).
16    pub code: u16,
17    /// Error class as a `u8` (see `gbp_core::ErrorClass`).
18    pub class: u8,
19    /// Whether the operation MAY be retried.
20    pub retryable: bool,
21    /// Whether the error is fatal (the node moves to FAILED).
22    pub fatal: bool,
23    /// Human-readable reason. MUST NOT carry secrets.
24    pub reason: String,
25    /// Structured detail bytes (typically CBOR).
26    #[serde(rename = "det")]
27    pub details_cbor: ByteBuf,
28}
29
30impl ErrorObject {
31    /// Builds an error object from a registry [`ErrorSpec`].
32    pub fn from_spec(spec: ErrorSpec, reason: impl Into<String>) -> Self {
33        Self {
34            code: spec.code,
35            class: spec.class as u8,
36            retryable: spec.retryable,
37            fatal: spec.fatal,
38            reason: reason.into(),
39            details_cbor: ByteBuf::new(),
40        }
41    }
42
43    /// Builds an error object with arbitrary fields (for codes outside the
44    /// registry).
45    pub fn new(
46        code: u16,
47        class: ErrorClass,
48        retryable: bool,
49        fatal: bool,
50        reason: impl Into<String>,
51    ) -> Self {
52        Self {
53            code,
54            class: class as u8,
55            retryable,
56            fatal,
57            reason: reason.into(),
58            details_cbor: ByteBuf::new(),
59        }
60    }
61
62    /// CBOR-encodes the error object.
63    pub fn to_cbor(&self) -> Vec<u8> {
64        let mut buf = Vec::new();
65        ciborium::into_writer(self, &mut buf).expect("cbor encode");
66        buf
67    }
68
69    /// Decodes a CBOR-encoded error object.
70    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
71        ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn new_round_trip() {
81        let err = ErrorObject::new(404, ErrorClass::Schema, true, false, "not found");
82        let bytes = err.to_cbor();
83        let decoded = ErrorObject::from_cbor(&bytes).unwrap();
84        assert_eq!(decoded.code, 404);
85        assert_eq!(decoded.class, ErrorClass::Schema as u8);
86        assert!(decoded.retryable);
87        assert!(!decoded.fatal);
88        assert_eq!(decoded.reason, "not found");
89        assert!(decoded.details_cbor.is_empty());
90    }
91
92    #[test]
93    fn fatal_error_round_trip() {
94        let err = ErrorObject::new(500, ErrorClass::Crypto, false, true, "aead failure");
95        let decoded = ErrorObject::from_cbor(&err.to_cbor()).unwrap();
96        assert_eq!(decoded.code, 500);
97        assert!(decoded.fatal);
98        assert!(!decoded.retryable);
99    }
100
101    #[test]
102    fn invalid_cbor_returns_decode_error() {
103        assert!(matches!(
104            ErrorObject::from_cbor(b"\xFF\xFF"),
105            Err(CodecError::Decode(_))
106        ));
107    }
108}