1#[derive(Debug, thiserror::Error)]
31pub enum XdsError {
32 #[error("invalid type URL: {type_url} - {reason}")]
34 InvalidTypeUrl {
35 type_url: String,
37 reason: String,
39 },
40
41 #[error("resource not found: {type_url}/{name}")]
43 ResourceNotFound {
44 type_url: String,
46 name: String,
48 },
49
50 #[error("version mismatch for {type_url}/{name}: expected {expected}, got {actual}")]
52 VersionMismatch {
53 type_url: String,
55 name: String,
57 expected: String,
59 actual: String,
61 },
62
63 #[error("invalid resource {type_url}/{name}: {reason}")]
65 InvalidResource {
66 type_url: String,
68 name: String,
70 reason: String,
72 },
73
74 #[error("cache error: {message}")]
76 CacheError {
77 message: String,
79 #[source]
81 source: Option<Box<dyn std::error::Error + Send + Sync>>,
82 },
83
84 #[error("watch error: {message}")]
86 WatchError {
87 message: String,
89 },
90
91 #[error("snapshot incomplete: missing {missing_types:?}")]
93 SnapshotIncomplete {
94 missing_types: Vec<String>,
96 },
97
98 #[error("encoding error for {type_url}: {message}")]
100 EncodingError {
101 type_url: String,
103 message: String,
105 },
106
107 #[error("decoding error for {type_url}: {message}")]
109 DecodingError {
110 type_url: String,
112 message: String,
114 },
115
116 #[error("transport error: {message}")]
118 TransportError {
119 message: String,
121 #[source]
123 source: Option<Box<dyn std::error::Error + Send + Sync>>,
124 },
125
126 #[error("stream closed: {reason}")]
128 StreamClosed {
129 reason: String,
131 },
132
133 #[error("NACK received from {node_id} for {type_url}: {error_message}")]
135 NackReceived {
136 node_id: String,
138 type_url: String,
140 nonce: String,
142 error_message: String,
144 },
145
146 #[error("operation timed out: {operation}")]
148 Timeout {
149 operation: String,
151 },
152
153 #[error("server is shutting down")]
155 Shutdown,
156
157 #[error("rate limited: {message}")]
159 RateLimited {
160 message: String,
162 },
163
164 #[error("internal error: {message}")]
166 Internal {
167 message: String,
169 #[source]
171 source: Option<Box<dyn std::error::Error + Send + Sync>>,
172 },
173
174 #[error("watch closed: watch_id={watch_id}")]
176 WatchClosed {
177 watch_id: u64,
179 },
180
181 #[error("configuration error: {0}")]
183 Configuration(String),
184}
185
186impl XdsError {
187 pub fn internal<E>(message: impl Into<String>, source: E) -> Self
189 where
190 E: std::error::Error + Send + Sync + 'static,
191 {
192 Self::Internal {
193 message: message.into(),
194 source: Some(Box::new(source)),
195 }
196 }
197
198 pub fn cache<E>(message: impl Into<String>, source: E) -> Self
200 where
201 E: std::error::Error + Send + Sync + 'static,
202 {
203 Self::CacheError {
204 message: message.into(),
205 source: Some(Box::new(source)),
206 }
207 }
208
209 pub fn transport<E>(message: impl Into<String>, source: E) -> Self
211 where
212 E: std::error::Error + Send + Sync + 'static,
213 {
214 Self::TransportError {
215 message: message.into(),
216 source: Some(Box::new(source)),
217 }
218 }
219}
220
221impl From<XdsError> for tonic::Status {
225 fn from(err: XdsError) -> Self {
226 match &err {
227 XdsError::InvalidTypeUrl { .. } | XdsError::InvalidResource { .. } => {
228 tonic::Status::invalid_argument(err.to_string())
229 }
230 XdsError::ResourceNotFound { .. } => tonic::Status::not_found(err.to_string()),
231 XdsError::VersionMismatch { .. } => tonic::Status::failed_precondition(err.to_string()),
232 XdsError::CacheError { .. }
233 | XdsError::WatchError { .. }
234 | XdsError::SnapshotIncomplete { .. } => tonic::Status::internal(err.to_string()),
235 XdsError::EncodingError { .. } | XdsError::DecodingError { .. } => {
236 tonic::Status::invalid_argument(err.to_string())
237 }
238 XdsError::TransportError { .. } | XdsError::StreamClosed { .. } => {
239 tonic::Status::unavailable(err.to_string())
240 }
241 XdsError::NackReceived { .. } => {
242 tonic::Status::ok(err.to_string())
244 }
245 XdsError::Timeout { .. } => tonic::Status::deadline_exceeded(err.to_string()),
246 XdsError::Shutdown => tonic::Status::unavailable(err.to_string()),
247 XdsError::RateLimited { .. } => tonic::Status::resource_exhausted(err.to_string()),
248 XdsError::Internal { .. } => tonic::Status::internal(err.to_string()),
249 XdsError::WatchClosed { .. } => tonic::Status::cancelled(err.to_string()),
250 XdsError::Configuration(_) => tonic::Status::invalid_argument(err.to_string()),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_error_display() {
261 let err = XdsError::ResourceNotFound {
262 type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster".to_string(),
263 name: "my-cluster".to_string(),
264 };
265 assert!(err.to_string().contains("my-cluster"));
266 }
267
268 #[test]
269 fn test_error_to_status() {
270 let err = XdsError::ResourceNotFound {
271 type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster".to_string(),
272 name: "my-cluster".to_string(),
273 };
274 let status: tonic::Status = err.into();
275 assert_eq!(status.code(), tonic::Code::NotFound);
276 }
277
278 #[test]
279 fn test_internal_error_helper() {
280 let io_err = std::io::Error::other("test error");
281 let err = XdsError::internal("operation failed", io_err);
282 assert!(matches!(err, XdsError::Internal { .. }));
283 }
284}