vgi_rpc/errors.rs
1//! Error types used throughout the vgi-rpc framework.
2
3use std::fmt;
4
5/// An RPC-level error, serialized on the wire as an EXCEPTION log batch.
6#[derive(Debug, Clone)]
7pub struct RpcError {
8 /// Error category (matches Python exception class names: "ValueError",
9 /// "RuntimeError", "TypeError", "ProtocolError", "VersionError", ...).
10 pub error_type: String,
11 /// Human-readable error message.
12 pub message: String,
13 /// Optional stack trace or remote traceback string.
14 pub traceback: String,
15 /// Optional request ID attached when the error was produced.
16 pub request_id: String,
17 /// Machine-readable reason when this error is an authentication
18 /// rejection. `None` means unclassified, which renders as
19 /// [`crate::unauthorized::AuthReason::Unauthorized`] — guessing a finer
20 /// code from an unclassified failure would mean matching on message
21 /// text.
22 pub auth_reason: Option<crate::unauthorized::AuthReason>,
23 /// `Retry-After` hint, in seconds, carried by a *transient* failure —
24 /// see [`RpcError::auth_unavailable`]. `None` on every other error.
25 pub retry_after_seconds: Option<u32>,
26}
27
28/// [`RpcError::error_type`] marking "I could not determine whether the
29/// credential is good", as distinct from "the credential is bad".
30///
31/// Mirrors the reference implementation's `AuthUnavailableError`, whose whole
32/// point is that it is *not* the rejection type: a chain that reads an outage
33/// as "not my credential, try the next" emerges as a 401 from the end of the
34/// chain, and a caller that negative-caches rejections then caches an outage.
35pub const AUTH_UNAVAILABLE_ERROR_TYPE: &str = "AuthUnavailableError";
36
37/// Default `Retry-After` for a transient authentication failure. Short on
38/// purpose: it is a hint to retry, not a backoff schedule.
39pub const DEFAULT_AUTH_RETRY_AFTER_SECONDS: u32 = 5;
40
41impl RpcError {
42 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
43 Self {
44 error_type: error_type.into(),
45 message: message.into(),
46 traceback: String::new(),
47 request_id: String::new(),
48 auth_reason: None,
49 retry_after_seconds: None,
50 }
51 }
52
53 /// An authenticator could not answer. **Not** a rejection.
54 ///
55 /// "The credential is bad" and "I could not find out whether the
56 /// credential is bad" are different answers, and collapsing them is
57 /// expensive in both directions. A sidecar restart surfacing as 401 makes
58 /// every caller re-authenticate at once; a caller that negative-caches
59 /// rejections will cache the outage and stay down after the sidecar comes
60 /// back.
61 ///
62 /// [`crate::auth::chain_authenticate`] propagates it — every `Err` from an
63 /// authenticator short-circuits the chain, so unlike the Python reference
64 /// there is no exception hierarchy to get wrong here; what the distinct
65 /// `error_type` buys is the HTTP mapping, which renders `503` +
66 /// `Retry-After` instead of `401`.
67 ///
68 /// Raise it for transport failures, timeouts, and 5xx from a remote
69 /// authority. Never for a credential the authority answered about.
70 pub fn auth_unavailable(detail: impl Into<String>) -> Self {
71 let mut err = Self::new(AUTH_UNAVAILABLE_ERROR_TYPE, detail);
72 err.retry_after_seconds = Some(DEFAULT_AUTH_RETRY_AFTER_SECONDS);
73 err
74 }
75
76 /// Override the `Retry-After` hint on a transient failure.
77 pub fn with_retry_after(mut self, seconds: u32) -> Self {
78 self.retry_after_seconds = Some(seconds);
79 self
80 }
81
82 /// Whether this is the transient "could not determine" signal rather than
83 /// a rejection.
84 pub fn is_auth_unavailable(&self) -> bool {
85 self.error_type == AUTH_UNAVAILABLE_ERROR_TYPE
86 }
87
88 /// Classify this error as an authentication rejection with `reason`.
89 ///
90 /// Returned from an authenticate callback, this is what lets the 401
91 /// carry a code a client can branch on rather than the
92 /// [`crate::unauthorized::AuthReason::Unauthorized`] fallback.
93 pub fn auth_failure(
94 reason: crate::unauthorized::AuthReason,
95 detail: impl Into<String>,
96 ) -> Self {
97 let mut err = Self::new("PermissionError", detail);
98 err.auth_reason = Some(reason);
99 err
100 }
101
102 pub fn value_error(msg: impl Into<String>) -> Self {
103 Self::new("ValueError", msg)
104 }
105
106 pub fn runtime_error(msg: impl Into<String>) -> Self {
107 Self::new("RuntimeError", msg)
108 }
109
110 pub fn type_error(msg: impl Into<String>) -> Self {
111 Self::new("TypeError", msg)
112 }
113
114 pub fn protocol_error(msg: impl Into<String>) -> Self {
115 Self::new("ProtocolError", msg)
116 }
117
118 pub fn version_error(msg: impl Into<String>) -> Self {
119 Self::new("VersionError", msg)
120 }
121
122 pub fn permission_error(msg: impl Into<String>) -> Self {
123 Self::new("PermissionError", msg)
124 }
125
126 pub fn attribute_error(msg: impl Into<String>) -> Self {
127 Self::new("AttributeError", msg)
128 }
129
130 /// Sticky-session token did not resolve to a live registry entry
131 /// (missing, expired, evicted, wrong worker, or principal mismatch).
132 /// Mirrors Python's `vgi_rpc.rpc.SessionLostError`.
133 pub fn session_lost_error(msg: impl Into<String>) -> Self {
134 Self::new("SessionLostError", msg)
135 }
136
137 /// Server is draining: new `ctx.open_session` calls are rejected while
138 /// existing sessions continue to serve. Mirrors Python's
139 /// `vgi_rpc.rpc.ServerDrainingError`.
140 pub fn server_draining_error(msg: impl Into<String>) -> Self {
141 Self::new("ServerDrainingError", msg)
142 }
143}
144
145impl fmt::Display for RpcError {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 write!(f, "{}: {}", self.error_type, self.message)
148 }
149}
150
151impl std::error::Error for RpcError {}
152
153/// Convenience alias for `Result<T, RpcError>`.
154pub type Result<T> = std::result::Result<T, RpcError>;
155
156impl From<arrow_schema::ArrowError> for RpcError {
157 fn from(e: arrow_schema::ArrowError) -> Self {
158 RpcError::new("ArrowError", e.to_string())
159 }
160}
161
162impl From<std::io::Error> for RpcError {
163 fn from(e: std::io::Error) -> Self {
164 RpcError::new("IOError", e.to_string())
165 }
166}