Skip to main content

brain_db_sdk/
error.rs

1//! Client error taxonomy.
2//!
3//! Every fallible client call returns [`Result`]. A [`BrainError`] separates
4//! the layers a caller reacts to differently: local I/O, a malformed frame or
5//! payload off the wire, a protocol-sequence violation, a structured error the
6//! server chose to return, version negotiation failure, a clean peer close, and
7//! a timeout. The server-returned [`BrainError::Server`] carries the full
8//! [`ErrorResponse`] so callers can branch on its stable code and category and
9//! honor `retry_after_ms` — which the [`crate::retry`] combinator consumes to
10//! back off retryable failures.
11
12use std::time::Duration;
13
14use crate::wire::cbor::CborError;
15use crate::wire::frame::FrameError;
16use crate::wire::types::{ErrorCategoryWire, ErrorCodeWire, ErrorResponse};
17
18/// Result alias for client operations.
19pub type Result<T> = std::result::Result<T, BrainError>;
20
21/// A failure from any client operation.
22#[derive(Debug, thiserror::Error)]
23pub enum BrainError {
24    /// Local socket I/O failed.
25    #[error("io error: {0}")]
26    Io(#[from] std::io::Error),
27
28    /// A frame off the wire failed to decode (bad magic / CRC / reserved
29    /// bits / oversize). A frame error desynchronizes the byte stream, so
30    /// the connection is no longer usable.
31    #[error("frame codec error: {0}")]
32    Frame(#[from] FrameError),
33
34    /// A frame's CBOR payload failed to encode or decode.
35    #[error("payload codec error: {0}")]
36    Cbor(#[from] CborError),
37
38    /// The peer broke the protocol sequence — e.g. an unexpected opcode
39    /// where the handshake expects WELCOME, or a response on an unknown
40    /// stream.
41    #[error("protocol violation: {0}")]
42    Protocol(String),
43
44    /// The server returned a structured ERROR frame in response to a
45    /// request. The full payload is preserved for code/category branching.
46    #[error("server error [{code:?} / {category:?}]: {message}")]
47    Server {
48        /// Specific error code; branch on this for programmatic handling.
49        code: ErrorCodeWire,
50        /// Coarse class of the failure, for deciding whether to retry at all.
51        category: ErrorCategoryWire,
52        /// Human-readable detail from the server.
53        message: String,
54        /// Server-supplied backoff hint, when it sent one.
55        retry_after_ms: Option<u32>,
56        /// The full decoded ERROR payload, kept so no field is lost to this
57        /// projection.
58        response: Box<ErrorResponse>,
59    },
60
61    /// The server chose a wire version the client does not support. Versions
62    /// are `u8` on the wire (HELLO `supported_versions` / WELCOME
63    /// `chosen_version`).
64    #[error("version mismatch: server chose {chosen}, client supports {supported:?}")]
65    VersionMismatch {
66        /// The version the server picked in WELCOME.
67        chosen: u8,
68        /// The versions this client offered in HELLO.
69        supported: Vec<u8>,
70    },
71
72    /// The peer closed the connection (read returned zero bytes).
73    #[error("connection closed by peer")]
74    Closed,
75
76    /// An operation did not complete within its deadline.
77    #[error("operation timed out after {0:?}")]
78    Timeout(Duration),
79}
80
81impl BrainError {
82    /// Build a [`BrainError::Server`] from a decoded ERROR payload.
83    #[must_use]
84    pub fn from_server(response: ErrorResponse) -> Self {
85        BrainError::Server {
86            code: response.code,
87            category: response.category,
88            message: response.message.clone(),
89            retry_after_ms: response.retry_after_ms,
90            response: Box::new(response),
91        }
92    }
93
94    /// Whether retrying the operation could plausibly succeed. A transient
95    /// transport drop or a resource-exhaustion / unavailable server verdict
96    /// is retryable; a malformed-input or auth verdict is not. Consumed by
97    /// [`crate::retry::with_retry`].
98    #[must_use]
99    pub fn is_retryable(&self) -> bool {
100        match self {
101            BrainError::Io(_) | BrainError::Closed | BrainError::Timeout(_) => true,
102            BrainError::Server { category, .. } => matches!(
103                category,
104                ErrorCategoryWire::ResourceExhausted | ErrorCategoryWire::Unavailable
105            ),
106            _ => false,
107        }
108    }
109
110    /// The server-requested cool-off before retrying, when the ERROR carried a
111    /// `retry_after_ms`. The retry combinator prefers this over its own
112    /// backoff schedule.
113    #[must_use]
114    pub fn retry_after(&self) -> Option<Duration> {
115        match self {
116            BrainError::Server {
117                retry_after_ms: Some(ms),
118                ..
119            } => Some(Duration::from_millis(u64::from(*ms))),
120            _ => None,
121        }
122    }
123}