Skip to main content

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}