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