#[derive(Debug, thiserror::Error)]
pub enum XdsError {
#[error("invalid type URL: {type_url} - {reason}")]
InvalidTypeUrl {
type_url: String,
reason: String,
},
#[error("resource not found: {type_url}/{name}")]
ResourceNotFound {
type_url: String,
name: String,
},
#[error("version mismatch for {type_url}/{name}: expected {expected}, got {actual}")]
VersionMismatch {
type_url: String,
name: String,
expected: String,
actual: String,
},
#[error("invalid resource {type_url}/{name}: {reason}")]
InvalidResource {
type_url: String,
name: String,
reason: String,
},
#[error("cache error: {message}")]
CacheError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("watch error: {message}")]
WatchError {
message: String,
},
#[error("snapshot incomplete: missing {missing_types:?}")]
SnapshotIncomplete {
missing_types: Vec<String>,
},
#[error("encoding error for {type_url}: {message}")]
EncodingError {
type_url: String,
message: String,
},
#[error("decoding error for {type_url}: {message}")]
DecodingError {
type_url: String,
message: String,
},
#[error("transport error: {message}")]
TransportError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("stream closed: {reason}")]
StreamClosed {
reason: String,
},
#[error("NACK received from {node_id} for {type_url}: {error_message}")]
NackReceived {
node_id: String,
type_url: String,
nonce: String,
error_message: String,
},
#[error("operation timed out: {operation}")]
Timeout {
operation: String,
},
#[error("server is shutting down")]
Shutdown,
#[error("rate limited: {message}")]
RateLimited {
message: String,
},
#[error("internal error: {message}")]
Internal {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("watch closed: watch_id={watch_id}")]
WatchClosed {
watch_id: u64,
},
#[error("configuration error: {0}")]
Configuration(String),
}
impl XdsError {
pub fn internal<E>(message: impl Into<String>, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Internal {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn cache<E>(message: impl Into<String>, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::CacheError {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn transport<E>(message: impl Into<String>, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::TransportError {
message: message.into(),
source: Some(Box::new(source)),
}
}
}
impl From<XdsError> for tonic::Status {
fn from(err: XdsError) -> Self {
match &err {
XdsError::InvalidTypeUrl { .. } | XdsError::InvalidResource { .. } => {
tonic::Status::invalid_argument(err.to_string())
}
XdsError::ResourceNotFound { .. } => tonic::Status::not_found(err.to_string()),
XdsError::VersionMismatch { .. } => tonic::Status::failed_precondition(err.to_string()),
XdsError::CacheError { .. }
| XdsError::WatchError { .. }
| XdsError::SnapshotIncomplete { .. } => tonic::Status::internal(err.to_string()),
XdsError::EncodingError { .. } | XdsError::DecodingError { .. } => {
tonic::Status::invalid_argument(err.to_string())
}
XdsError::TransportError { .. } | XdsError::StreamClosed { .. } => {
tonic::Status::unavailable(err.to_string())
}
XdsError::NackReceived { .. } => {
tonic::Status::ok(err.to_string())
}
XdsError::Timeout { .. } => tonic::Status::deadline_exceeded(err.to_string()),
XdsError::Shutdown => tonic::Status::unavailable(err.to_string()),
XdsError::RateLimited { .. } => tonic::Status::resource_exhausted(err.to_string()),
XdsError::Internal { .. } => tonic::Status::internal(err.to_string()),
XdsError::WatchClosed { .. } => tonic::Status::cancelled(err.to_string()),
XdsError::Configuration(_) => tonic::Status::invalid_argument(err.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = XdsError::ResourceNotFound {
type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster".to_string(),
name: "my-cluster".to_string(),
};
assert!(err.to_string().contains("my-cluster"));
}
#[test]
fn test_error_to_status() {
let err = XdsError::ResourceNotFound {
type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster".to_string(),
name: "my-cluster".to_string(),
};
let status: tonic::Status = err.into();
assert_eq!(status.code(), tonic::Code::NotFound);
}
#[test]
fn test_internal_error_helper() {
let io_err = std::io::Error::other("test error");
let err = XdsError::internal("operation failed", io_err);
assert!(matches!(err, XdsError::Internal { .. }));
}
}