osproxy_spi/error.rs
1//! The error type an SPI implementation returns.
2//!
3//! Every variant maps to a stable [`ErrorCode`] and carries shape-only context
4//! (which sources were tried, which partition id) so a failure is diagnosable
5//! from telemetry without reading source (NFR-T5, `docs/02` §4). It never
6//! carries tenant *values*.
7
8use osproxy_core::{ErrorCode, PartitionId};
9use thiserror::Error;
10
11use crate::rules::PartitionKeySpecKind;
12
13/// A failure returned by [`RoutingSpi`] or [`TenancySpi`].
14///
15/// [`RoutingSpi`]: crate::RoutingSpi
16/// [`TenancySpi`]: crate::TenancySpi
17///
18/// # Examples
19///
20/// ```
21/// use osproxy_spi::SpiError;
22/// use osproxy_spi::core::ErrorCode;
23///
24/// let err = SpiError::PlacementBackend { retryable: true };
25/// assert_eq!(err.code(), ErrorCode::PlacementBackendUnavailable);
26/// assert!(err.retryable());
27/// ```
28#[non_exhaustive]
29#[derive(Debug, Error)]
30pub enum SpiError {
31 /// The partition could not be resolved from the request. Reports which
32 /// source kinds were attempted (shape only).
33 #[error("partition could not be resolved (tried: {tried:?})")]
34 PartitionUnresolved {
35 /// The source kinds tried, in order, before giving up.
36 tried: Vec<PartitionKeySpecKind>,
37 },
38
39 /// No placement exists for the resolved partition.
40 #[error("no placement exists for partition")]
41 PlacementMissing {
42 /// The unresolved partition (an id, safe in telemetry).
43 partition: PartitionId,
44 },
45
46 /// The placement-lookup backend was unavailable.
47 #[error("placement lookup backend unavailable (retryable={retryable})")]
48 PlacementBackend {
49 /// Whether the caller may retry the lookup.
50 retryable: bool,
51 },
52
53 /// The request endpoint is not supported for tenancy rewriting in this mode.
54 #[error("endpoint not supported for tenancy rewrite")]
55 UnsupportedEndpoint {
56 /// The endpoint classification that was rejected.
57 endpoint: osproxy_core::EndpointKind,
58 },
59
60 /// An injected field draws its value from a principal attribute that the
61 /// authenticated principal does not carry. A configuration/identity
62 /// mismatch, surfaced as a routing failure rather than silently injecting a
63 /// null (which would corrupt isolation).
64 #[error("principal is missing an attribute required by an injected field")]
65 PrincipalAttrMissing {
66 /// The missing attribute name.
67 attr: String,
68 },
69
70 /// An injected field draws its value from a request header the request does
71 /// not carry. Surfaced as a routing failure rather than injecting a null.
72 #[error("request is missing a header required by an injected field")]
73 HeaderMissing {
74 /// The missing header name.
75 header: String,
76 },
77
78 /// A `SharedIndex` placement was configured with a doc-id rule that does not
79 /// include the partition id, which would allow cross-tenant id collisions
80 /// (`docs/03`). A configuration error, surfaced as a routing failure.
81 #[error("shared-index doc-id rule must reference the partition id")]
82 IdRuleMissingPartition,
83}
84
85impl SpiError {
86 /// The stable [`ErrorCode`] for this failure, for trace attributes and
87 /// `/debug/explain`.
88 #[must_use]
89 pub fn code(&self) -> ErrorCode {
90 match self {
91 Self::PartitionUnresolved { .. } => ErrorCode::PartitionUnresolved,
92 Self::PlacementMissing { .. } => ErrorCode::PlacementMissing,
93 Self::PlacementBackend { .. } => ErrorCode::PlacementBackendUnavailable,
94 // IdRuleMissingPartition is a misconfiguration that prevents safe
95 // routing; reuse the unsupported-endpoint contract code until a
96 // dedicated config code is added (additive, see docs/08 §7).
97 Self::UnsupportedEndpoint { .. }
98 | Self::IdRuleMissingPartition
99 | Self::PrincipalAttrMissing { .. }
100 | Self::HeaderMissing { .. } => ErrorCode::UnsupportedEndpoint,
101 }
102 }
103
104 /// Whether the caller may retry, possibly after re-resolving placement.
105 #[must_use]
106 pub fn retryable(&self) -> bool {
107 matches!(self, Self::PlacementBackend { retryable: true })
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn codes_map_to_core_taxonomy() {
117 assert_eq!(
118 SpiError::PartitionUnresolved { tried: vec![] }.code(),
119 ErrorCode::PartitionUnresolved
120 );
121 assert_eq!(
122 SpiError::PlacementMissing {
123 partition: PartitionId::from("p")
124 }
125 .code(),
126 ErrorCode::PlacementMissing
127 );
128 }
129
130 #[test]
131 fn only_backend_unavailable_is_retryable() {
132 assert!(SpiError::PlacementBackend { retryable: true }.retryable());
133 assert!(!SpiError::PlacementBackend { retryable: false }.retryable());
134 assert!(!SpiError::PlacementMissing {
135 partition: PartitionId::from("p")
136 }
137 .retryable());
138 }
139}