diff --git a/tonic/src/transport/channel/endpoint.rs b/tonic/src/transport/channel/endpoint.rs index 4a63746ce..09a550819 100644 --- a/tonic/src/transport/channel/endpoint.rs +++ b/tonic/src/transport/channel/endpoint.rs @@ -380,7 +380,7 @@ impl Endpoint { ), ..self }), - EndpointType::Uds(_) => Err(Error::new(error::Kind::InvalidTlsConfigForUds)), + EndpointType::Uds(_) => Err(Error::new(error::ErrorKind::InvalidTlsConfigForUds)), } } @@ -420,7 +420,7 @@ impl Endpoint { ), ..self }), - EndpointType::Uds(_) => Err(Error::new(error::Kind::InvalidTlsConfigForUds)), + EndpointType::Uds(_) => Err(Error::new(error::ErrorKind::InvalidTlsConfigForUds)), } } diff --git a/tonic/src/transport/error.rs b/tonic/src/transport/error.rs index 31b317521..d113e7317 100644 --- a/tonic/src/transport/error.rs +++ b/tonic/src/transport/error.rs @@ -8,23 +8,40 @@ pub struct Error { } struct ErrorImpl { - kind: Kind, + kind: ErrorKind, source: Option, } -#[derive(Debug)] -pub(crate) enum Kind { +/// A categorical description of a [`transport::Error`](Error). +/// +/// Returned by [`Error::kind`], this enum lets callers programmatically +/// distinguish between the different failure modes of the transport layer +/// without inspecting the error's `Display` output or downcasting its +/// [`source`](std::error::Error::source). +/// +/// This enum is marked `#[non_exhaustive]`: new variants may be added in +/// the future without a major version bump, so always include a `_ =>` +/// arm when matching on it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ErrorKind { + /// A generic transport-level failure (I/O, protocol, TLS handshake, etc.). + /// The underlying cause is available via [`std::error::Error::source`]. Transport, + /// The provided value could not be parsed as a valid URI. #[cfg(feature = "channel")] InvalidUri, + /// The configured user-agent string is not a valid HTTP header value. #[cfg(feature = "channel")] InvalidUserAgent, + /// TLS configuration was applied to an endpoint that uses a Unix domain + /// socket, which is not supported. #[cfg(all(feature = "_tls-any", feature = "channel"))] InvalidTlsConfigForUds, } impl Error { - pub(crate) fn new(kind: Kind) -> Self { + pub(crate) fn new(kind: ErrorKind) -> Self { Self { inner: ErrorImpl { kind, source: None }, } @@ -36,28 +53,47 @@ impl Error { } pub(crate) fn from_source(source: impl Into) -> Self { - Error::new(Kind::Transport).with(source) + Error::new(ErrorKind::Transport).with(source) } #[cfg(feature = "channel")] pub(crate) fn new_invalid_uri() -> Self { - Error::new(Kind::InvalidUri) + Error::new(ErrorKind::InvalidUri) } #[cfg(feature = "channel")] pub(crate) fn new_invalid_user_agent() -> Self { - Error::new(Kind::InvalidUserAgent) + Error::new(ErrorKind::InvalidUserAgent) + } + + /// Returns the [`ErrorKind`] categorizing this error. + /// + /// Use this to branch on the failure mode without parsing `Display` + /// output. Always include a `_` arm — [`ErrorKind`] is `#[non_exhaustive]`. + /// + /// ```ignore + /// use tonic::transport::{Error, ErrorKind}; + /// + /// fn classify(err: &Error) { + /// match err.kind() { + /// ErrorKind::Transport => { /* network-level failure */ } + /// _ => { /* configuration or other error */ } + /// } + /// } + /// ``` + pub fn kind(&self) -> ErrorKind { + self.inner.kind } fn description(&self) -> &str { match &self.inner.kind { - Kind::Transport => "transport error", + ErrorKind::Transport => "transport error", #[cfg(feature = "channel")] - Kind::InvalidUri => "invalid URI", + ErrorKind::InvalidUri => "invalid URI", #[cfg(feature = "channel")] - Kind::InvalidUserAgent => "user agent is not a valid header value", + ErrorKind::InvalidUserAgent => "user agent is not a valid header value", #[cfg(all(feature = "_tls-any", feature = "channel"))] - Kind::InvalidTlsConfigForUds => "cannot apply TLS config for unix domain socket", + ErrorKind::InvalidTlsConfigForUds => "cannot apply TLS config for unix domain socket", } } } @@ -90,3 +126,36 @@ impl StdError for Error { .map(|source| &**source as &(dyn StdError + 'static)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_source_has_transport_kind() { + let inner = std::io::Error::other("boom"); + let err = Error::from_source(inner); + assert_eq!(err.kind(), ErrorKind::Transport); + } + + #[cfg(feature = "channel")] + #[test] + fn invalid_uri_kind() { + assert_eq!(Error::new_invalid_uri().kind(), ErrorKind::InvalidUri); + } + + #[cfg(feature = "channel")] + #[test] + fn invalid_user_agent_kind() { + assert_eq!( + Error::new_invalid_user_agent().kind(), + ErrorKind::InvalidUserAgent, + ); + } + + #[test] + fn error_kind_is_copy() { + fn assert_copy() {} + assert_copy::(); + } +} diff --git a/tonic/src/transport/mod.rs b/tonic/src/transport/mod.rs index 4c1f68d26..44a065ba1 100644 --- a/tonic/src/transport/mod.rs +++ b/tonic/src/transport/mod.rs @@ -101,7 +101,7 @@ mod tls; #[doc(inline)] #[cfg(feature = "channel")] pub use self::channel::{Channel, Endpoint}; -pub use self::error::Error; +pub use self::error::{Error, ErrorKind}; #[doc(inline)] #[cfg(feature = "server")] pub use self::server::Server;