use thiserror::Error;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum GrpcStatusCode {
Ok,
Cancelled,
Unknown,
InvalidArgument,
DeadlineExceeded,
NotFound,
AlreadyExists,
PermissionDenied,
ResourceExhausted,
FailedPrecondition,
Aborted,
OutOfRange,
Unimplemented,
Internal,
Unavailable,
DataLoss,
Unauthenticated,
}
impl GrpcStatusCode {
#[cfg(any(feature = "workload-api-x509", feature = "workload-api-jwt"))]
const fn from_tonic(code: tonic::Code) -> Self {
match code {
tonic::Code::Ok => Self::Ok,
tonic::Code::Cancelled => Self::Cancelled,
tonic::Code::Unknown => Self::Unknown,
tonic::Code::InvalidArgument => Self::InvalidArgument,
tonic::Code::DeadlineExceeded => Self::DeadlineExceeded,
tonic::Code::NotFound => Self::NotFound,
tonic::Code::AlreadyExists => Self::AlreadyExists,
tonic::Code::PermissionDenied => Self::PermissionDenied,
tonic::Code::ResourceExhausted => Self::ResourceExhausted,
tonic::Code::FailedPrecondition => Self::FailedPrecondition,
tonic::Code::Aborted => Self::Aborted,
tonic::Code::OutOfRange => Self::OutOfRange,
tonic::Code::Unimplemented => Self::Unimplemented,
tonic::Code::Internal => Self::Internal,
tonic::Code::Unavailable => Self::Unavailable,
tonic::Code::DataLoss => Self::DataLoss,
tonic::Code::Unauthenticated => Self::Unauthenticated,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Cancelled => "cancelled",
Self::Unknown => "unknown",
Self::InvalidArgument => "invalid_argument",
Self::DeadlineExceeded => "deadline_exceeded",
Self::NotFound => "not_found",
Self::AlreadyExists => "already_exists",
Self::PermissionDenied => "permission_denied",
Self::ResourceExhausted => "resource_exhausted",
Self::FailedPrecondition => "failed_precondition",
Self::Aborted => "aborted",
Self::OutOfRange => "out_of_range",
Self::Unimplemented => "unimplemented",
Self::Internal => "internal",
Self::Unavailable => "unavailable",
Self::DataLoss => "data_loss",
Self::Unauthenticated => "unauthenticated",
}
}
}
impl std::fmt::Display for GrpcStatusCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub struct GrpcStatusError {
code: GrpcStatusCode,
message: Box<str>,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl GrpcStatusError {
fn new(code: GrpcStatusCode, message: impl Into<Box<str>>) -> Self {
Self {
code,
message: message.into(),
source: None,
}
}
fn with_source(
code: GrpcStatusCode,
message: impl Into<Box<str>>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self {
code,
message: message.into(),
source: Some(Box::new(source)),
}
}
pub const fn code(&self) -> GrpcStatusCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for GrpcStatusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "gRPC status {}: {}", self.code, self.message)
}
}
impl std::error::Error for GrpcStatusError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self.source.as_deref() {
Some(source) => Some(source),
None => None,
}
}
}
#[derive(Debug)]
pub struct TransportConnectError {
source: Box<dyn std::error::Error + Send + Sync>,
}
impl TransportConnectError {
fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
Self {
source: Box::new(source),
}
}
}
impl std::fmt::Display for TransportConnectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "gRPC transport connection failed: {}", self.source)
}
}
impl std::error::Error for TransportConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TransportError {
#[error("unsupported endpoint transport: {scheme}")]
UnsupportedEndpointTransport {
scheme: &'static str,
},
#[error(transparent)]
Status(#[from] GrpcStatusError),
#[error(transparent)]
Connect(#[from] TransportConnectError),
}
impl TransportError {
pub fn status(code: GrpcStatusCode, message: impl Into<Box<str>>) -> Self {
Self::Status(GrpcStatusError::new(code, message))
}
pub fn status_with_source(
code: GrpcStatusCode,
message: impl Into<Box<str>>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Status(GrpcStatusError::with_source(code, message, source))
}
pub fn connect(source: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::Connect(TransportConnectError::new(source))
}
#[cfg(any(feature = "workload-api-x509", feature = "workload-api-jwt"))]
pub(crate) fn from_status(source: tonic::Status) -> Self {
let code = GrpcStatusCode::from_tonic(source.code());
let message = source.message().to_owned();
Self::status_with_source(code, message, source)
}
pub(crate) fn from_transport(source: tonic::transport::Error) -> Self {
Self::connect(source)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn status_display_uses_owned_code_and_message() {
let err = TransportError::status(GrpcStatusCode::Unavailable, "agent unavailable");
assert_eq!(
err.to_string(),
"gRPC status unavailable: agent unavailable"
);
let TransportError::Status(status) = &err else {
panic!("expected status error");
};
assert_eq!(status.code(), GrpcStatusCode::Unavailable);
assert_eq!(status.message(), "agent unavailable");
assert!(status.source().is_none());
}
#[test]
fn status_with_source_preserves_authoritative_fields_and_diagnostics() {
let err = TransportError::status_with_source(
GrpcStatusCode::Unavailable,
"agent unavailable",
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "socket refused"),
);
assert_eq!(
err.to_string(),
"gRPC status unavailable: agent unavailable"
);
assert_eq!(
err.source().expect("diagnostic source").to_string(),
"socket refused"
);
let TransportError::Status(status) = err else {
panic!("expected status error");
};
assert_eq!(status.code(), GrpcStatusCode::Unavailable);
assert_eq!(status.message(), "agent unavailable");
}
#[cfg(any(feature = "workload-api-x509", feature = "workload-api-jwt"))]
#[test]
fn internally_converted_status_retains_diagnostic_source() {
let err = TransportError::from_status(tonic::Status::unavailable("agent unavailable"));
let TransportError::Status(status) = &err else {
panic!("expected status error");
};
let foreign = status.source().expect("foreign status source");
assert!(foreign.to_string().contains("agent unavailable"));
}
#[test]
fn connect_display_is_dependency_independent() {
let failure = tonic::transport::Endpoint::from_shared("http://")
.expect_err("invalid endpoint should fail");
let expected = failure.to_string();
let err = TransportError::from_transport(failure);
assert_eq!(
err.to_string(),
format!("gRPC transport connection failed: {expected}")
);
let TransportError::Connect(wrapper) = &err else {
panic!("expected connection error");
};
assert_eq!(
wrapper.source().expect("foreign source").to_string(),
expected
);
}
}