use super::CredentialsError;
use super::rpc::Status;
use http::HeaderMap;
use std::error::Error as StdError;
type BoxError = Box<dyn StdError + Send + Sync>;
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
source: Option<BoxError>,
}
impl Error {
pub fn service(status: Status) -> Self {
let details = ServiceDetails {
status,
status_code: None,
headers: None,
};
Self {
kind: ErrorKind::Service(Box::new(details)),
source: None,
}
}
pub fn timeout<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Timeout,
source: Some(source.into()),
}
}
pub fn is_timeout(&self) -> bool {
matches!(self.kind, ErrorKind::Timeout)
}
pub fn exhausted<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Exhausted,
source: Some(source.into()),
}
}
pub fn is_exhausted(&self) -> bool {
matches!(self.kind, ErrorKind::Exhausted)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn deser<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Deserialization,
source: Some(source.into()),
}
}
pub fn is_deserialization(&self) -> bool {
matches!(self.kind, ErrorKind::Deserialization)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn ser<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Serialization,
source: Some(source.into()),
}
}
pub fn is_serialization(&self) -> bool {
matches!(self.kind, ErrorKind::Serialization)
}
pub fn status(&self) -> Option<&Status> {
match &self.kind {
ErrorKind::Service(d) => Some(&d.as_ref().status),
_ => None,
}
}
pub fn http_status_code(&self) -> Option<u16> {
match &self.kind {
ErrorKind::Transport(d) => d.as_ref().status_code,
ErrorKind::Service(d) => d.as_ref().status_code,
_ => None,
}
}
pub fn http_headers(&self) -> Option<&http::HeaderMap> {
match &self.kind {
ErrorKind::Transport(d) => d.as_ref().headers.as_ref(),
ErrorKind::Service(d) => d.as_ref().headers.as_ref(),
_ => None,
}
}
pub fn http_payload(&self) -> Option<&bytes::Bytes> {
match &self.kind {
ErrorKind::Transport(d) => d.payload.as_ref(),
_ => None,
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn service_with_http_metadata(
status: Status,
status_code: Option<u16>,
headers: Option<http::HeaderMap>,
) -> Self {
Self::service_full(status, status_code, headers, None)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn service_full(
status: Status,
status_code: Option<u16>,
headers: Option<http::HeaderMap>,
source: Option<BoxError>,
) -> Self {
let details = ServiceDetails {
status_code,
headers,
status,
};
let kind = ErrorKind::Service(Box::new(details));
Self { kind, source }
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn binding<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Binding,
source: Some(source.into()),
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_binding(&self) -> bool {
matches!(&self.kind, ErrorKind::Binding)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn authentication(source: CredentialsError) -> Self {
Self {
kind: ErrorKind::Authentication,
source: Some(source.into()),
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_authentication(&self) -> bool {
matches!(self.kind, ErrorKind::Authentication)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn http(status_code: u16, headers: HeaderMap, payload: bytes::Bytes) -> Self {
let details = TransportDetails {
status_code: Some(status_code),
headers: Some(headers),
payload: Some(payload),
};
let kind = ErrorKind::Transport(Box::new(details));
Self { kind, source: None }
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn io<T: Into<BoxError>>(source: T) -> Self {
let details = TransportDetails {
status_code: None,
headers: None,
payload: None,
};
Self {
kind: ErrorKind::Transport(Box::new(details)),
source: Some(source.into()),
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_io(&self) -> bool {
matches!(
&self.kind,
ErrorKind::Transport(d) if matches!(**d, TransportDetails {
status_code: None,
headers: None,
payload: None,
..
}))
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn connect<T: Into<BoxError>>(source: T) -> Self {
Self {
kind: ErrorKind::Connect,
source: Some(source.into()),
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_connect(&self) -> bool {
matches!(&self.kind, ErrorKind::Connect)
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn transport<T: Into<BoxError>>(headers: HeaderMap, source: T) -> Self {
let details = TransportDetails {
headers: Some(headers),
status_code: None,
payload: None,
};
Self {
kind: ErrorKind::Transport(Box::new(details)),
source: Some(source.into()),
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_transport(&self) -> bool {
matches!(&self.kind, ErrorKind::Transport { .. })
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
pub fn is_transient_and_before_rpc(&self) -> bool {
match &self.kind {
ErrorKind::Connect => true,
ErrorKind::Authentication => self
.source
.as_ref()
.and_then(|e| e.downcast_ref::<CredentialsError>())
.map(|e| e.is_transient())
.unwrap_or(false),
_ => false,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.kind, &self.source) {
(ErrorKind::Binding, Some(e)) => {
write!(f, "cannot find a matching binding to send the request: {e}")
}
(ErrorKind::Serialization, Some(e)) => write!(f, "cannot serialize the request {e}"),
(ErrorKind::Deserialization, Some(e)) => {
write!(f, "cannot deserialize the response {e}")
}
(ErrorKind::Authentication, Some(e)) => {
write!(f, "cannot create the authentication headers {e}")
}
(ErrorKind::Timeout, Some(e)) => {
write!(f, "the request exceeded the request deadline {e}")
}
(ErrorKind::Exhausted, Some(e)) => {
write!(f, "{e}")
}
(ErrorKind::Connect, Some(e)) => {
write!(f, "cannot connect to endpoint: {e}")
}
(ErrorKind::Transport(details), _) => details.display(self.source(), f),
(ErrorKind::Service(d), _) => {
write!(
f,
"the service reports an error with code {} described as: {}",
d.status.code, d.status.message
)
}
(_, None) => unreachable!("no constructor allows this"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &dyn std::error::Error)
}
}
#[derive(Debug)]
enum ErrorKind {
Binding,
Serialization,
Deserialization,
Authentication,
Timeout,
Exhausted,
Connect,
Transport(Box<TransportDetails>),
Service(Box<ServiceDetails>),
}
#[derive(Debug)]
struct TransportDetails {
status_code: Option<u16>,
headers: Option<HeaderMap>,
payload: Option<bytes::Bytes>,
}
impl TransportDetails {
fn display(
&self,
source: Option<&(dyn StdError + 'static)>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match (source, &self) {
(
_,
TransportDetails {
status_code: Some(code),
payload: Some(p),
..
},
) => {
if let Ok(message) = std::str::from_utf8(p.as_ref()) {
write!(f, "the HTTP transport reports a [{code}] error: {message}")
} else {
write!(f, "the HTTP transport reports a [{code}] error: {p:?}")
}
}
(Some(source), _) => {
write!(f, "the transport reports an error: {source}")
}
(None, _) => unreachable!("no Error constructor allows this"),
}
}
}
#[derive(Debug)]
struct ServiceDetails {
status_code: Option<u16>,
headers: Option<HeaderMap>,
status: Status,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CredentialsError;
use crate::error::rpc::Code;
use std::error::Error as StdError;
#[test]
fn service() {
let status = Status::default()
.set_code(Code::NotFound)
.set_message("NOT FOUND");
let error = Error::service(status.clone());
assert!(error.source().is_none(), "{error:?}");
assert_eq!(error.status(), Some(&status));
assert!(error.to_string().contains("NOT FOUND"), "{error}");
assert!(error.to_string().contains(Code::NotFound.name()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn timeout() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::timeout(source);
assert!(error.is_timeout(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
assert!(error.http_headers().is_none(), "{error:?}");
assert!(error.http_status_code().is_none(), "{error:?}");
assert!(error.http_payload().is_none(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
}
#[test]
fn exhausted() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::exhausted(source);
assert!(error.is_exhausted(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
assert!(error.http_headers().is_none(), "{error:?}");
assert!(error.http_status_code().is_none(), "{error:?}");
assert!(error.http_payload().is_none(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
}
#[test]
fn serialization() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::deser(source);
assert!(error.is_deserialization(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn connect() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::connect(source);
assert!(error.is_connect(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(error.is_transient_and_before_rpc(), "{error:?}");
assert!(error.http_headers().is_none(), "{error:?}");
assert!(error.http_status_code().is_none(), "{error:?}");
assert!(error.http_payload().is_none(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
}
#[test]
fn service_with_http_metadata() {
let status = Status::default()
.set_code(Code::NotFound)
.set_message("NOT FOUND");
let status_code = 404_u16;
let headers = {
let mut headers = http::HeaderMap::new();
headers.insert(
"content-type",
http::HeaderValue::from_static("application/json"),
);
headers
};
let error = Error::service_with_http_metadata(
status.clone(),
Some(status_code),
Some(headers.clone()),
);
assert_eq!(error.status(), Some(&status));
assert!(error.to_string().contains("NOT FOUND"), "{error}");
assert!(error.to_string().contains(Code::NotFound.name()), "{error}");
assert_eq!(error.http_status_code(), Some(status_code));
assert_eq!(error.http_headers(), Some(&headers));
assert!(error.http_payload().is_none(), "{error:?}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn service_full() {
let status = Status::default()
.set_code(Code::NotFound)
.set_message("NOT FOUND");
let status_code = 404_u16;
let headers = {
let mut headers = http::HeaderMap::new();
headers.insert(
"content-type",
http::HeaderValue::from_static("application/json"),
);
headers
};
let error = Error::service_full(
status.clone(),
Some(status_code),
Some(headers.clone()),
Some(Box::new(wkt::TimestampError::OutOfRange)),
);
assert_eq!(error.status(), Some(&status));
assert!(error.to_string().contains("NOT FOUND"), "{error}");
assert!(error.to_string().contains(Code::NotFound.name()), "{error}");
assert_eq!(error.http_status_code(), Some(status_code));
assert_eq!(error.http_headers(), Some(&headers));
assert!(error.http_payload().is_none(), "{error:?}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
}
#[test]
fn binding() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::binding(source);
assert!(error.is_binding(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
assert!(error.http_status_code().is_none(), "{error:?}");
assert!(error.http_headers().is_none(), "{error:?}");
assert!(error.http_payload().is_none(), "{error:?}");
}
#[test]
fn ser() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::ser(source);
assert!(error.is_serialization(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn auth_transient() {
let source = CredentialsError::from_msg(true, "test-message");
let error = Error::authentication(source);
assert!(error.is_authentication(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<CredentialsError>());
assert!(matches!(got, Some(c) if c.is_transient()), "{error:?}");
assert!(error.to_string().contains("test-message"), "{error}");
assert!(error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn auth_not_transient() {
let source = CredentialsError::from_msg(false, "test-message");
let error = Error::authentication(source);
assert!(error.is_authentication(), "{error:?}");
assert!(error.source().is_some(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<CredentialsError>());
assert!(matches!(got, Some(c) if !c.is_transient()), "{error:?}");
assert!(error.to_string().contains("test-message"), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn http() {
let status_code = 404_u16;
let headers = {
let mut headers = http::HeaderMap::new();
headers.insert(
"content-type",
http::HeaderValue::from_static("application/json"),
);
headers
};
let payload = bytes::Bytes::from_static(b"NOT FOUND");
let error = Error::http(status_code, headers.clone(), payload.clone());
assert!(error.is_transport(), "{error:?}");
assert!(!error.is_io(), "{error:?}");
assert!(error.source().is_none(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
assert!(error.to_string().contains("NOT FOUND"), "{error}");
assert!(error.to_string().contains("404"), "{error}");
assert_eq!(error.http_status_code(), Some(status_code));
assert_eq!(error.http_headers(), Some(&headers));
assert_eq!(error.http_payload(), Some(&payload));
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn http_binary() {
let status_code = 404_u16;
let headers = {
let mut headers = http::HeaderMap::new();
headers.insert(
"content-type",
http::HeaderValue::from_static("application/json"),
);
headers
};
let payload = bytes::Bytes::from_static(&[0xFF, 0xFF]);
let error = Error::http(status_code, headers.clone(), payload.clone());
assert!(error.is_transport(), "{error:?}");
assert!(!error.is_io(), "{error:?}");
assert!(error.source().is_none(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
assert!(
error.to_string().contains(&format! {"{payload:?}"}),
"{error}"
);
assert!(error.to_string().contains("404"), "{error}");
assert_eq!(error.http_status_code(), Some(status_code));
assert_eq!(error.http_headers(), Some(&headers));
assert_eq!(error.http_payload(), Some(&payload));
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn io() {
let source = wkt::TimestampError::OutOfRange;
let error = Error::io(source);
assert!(error.is_transport(), "{error:?}");
assert!(error.is_io(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
let got = error
.source()
.and_then(|e| e.downcast_ref::<wkt::TimestampError>());
assert!(
matches!(got, Some(wkt::TimestampError::OutOfRange)),
"{error:?}"
);
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
#[test]
fn transport() {
let headers = {
let mut headers = http::HeaderMap::new();
headers.insert(
"content-type",
http::HeaderValue::from_static("application/json"),
);
headers
};
let source = wkt::TimestampError::OutOfRange;
let error = Error::transport(headers.clone(), source);
assert!(error.is_transport(), "{error:?}");
assert!(!error.is_io(), "{error:?}");
assert!(error.status().is_none(), "{error:?}");
let source = wkt::TimestampError::OutOfRange;
assert!(error.to_string().contains(&source.to_string()), "{error}");
assert!(error.http_status_code().is_none(), "{error:?}");
assert_eq!(error.http_headers(), Some(&headers));
assert!(error.http_payload().is_none(), "{error:?}");
assert!(!error.is_transient_and_before_rpc(), "{error:?}");
}
}