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}