use crate::metadata::GRPC_CONTENT_TYPE;
use crate::metadata::MetadataMap;
use base64::Engine as _;
use bytes::Bytes;
use http::{
HeaderName,
header::{HeaderMap, HeaderValue},
};
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode};
use std::{borrow::Cow, error::Error, fmt, sync::Arc};
use tracing::{debug, trace, warn};
const ENCODING_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'<')
.add(b'>')
.add(b'`')
.add(b'?')
.add(b'{')
.add(b'}');
#[derive(Clone)]
pub struct Status(Box<StatusInner>);
#[derive(Clone)]
struct StatusInner {
code: Code,
message: String,
details: Bytes,
metadata: MetadataMap,
source: Option<Arc<dyn Error + Send + Sync + 'static>>,
}
impl StatusInner {
fn into_status(self) -> Status {
Status(Box::new(self))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Code {
Ok = 0,
Cancelled = 1,
Unknown = 2,
InvalidArgument = 3,
DeadlineExceeded = 4,
NotFound = 5,
AlreadyExists = 6,
PermissionDenied = 7,
ResourceExhausted = 8,
FailedPrecondition = 9,
Aborted = 10,
OutOfRange = 11,
Unimplemented = 12,
Internal = 13,
Unavailable = 14,
DataLoss = 15,
Unauthenticated = 16,
}
impl Code {
pub fn description(&self) -> &'static str {
match self {
Code::Ok => "The operation completed successfully",
Code::Cancelled => "The operation was cancelled",
Code::Unknown => "Unknown error",
Code::InvalidArgument => "Client specified an invalid argument",
Code::DeadlineExceeded => "Deadline expired before operation could complete",
Code::NotFound => "Some requested entity was not found",
Code::AlreadyExists => "Some entity that we attempted to create already exists",
Code::PermissionDenied => {
"The caller does not have permission to execute the specified operation"
}
Code::ResourceExhausted => "Some resource has been exhausted",
Code::FailedPrecondition => {
"The system is not in a state required for the operation's execution"
}
Code::Aborted => "The operation was aborted",
Code::OutOfRange => "Operation was attempted past the valid range",
Code::Unimplemented => "Operation is not implemented or not supported",
Code::Internal => "Internal error",
Code::Unavailable => "The service is currently unavailable",
Code::DataLoss => "Unrecoverable data loss or corruption",
Code::Unauthenticated => "The request does not have valid authentication credentials",
}
}
}
impl std::fmt::Display for Code {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.description(), f)
}
}
impl Status {
pub fn new(code: Code, message: impl Into<String>) -> Status {
StatusInner {
code,
message: message.into(),
details: Bytes::new(),
metadata: MetadataMap::new(),
source: None,
}
.into_status()
}
pub fn ok(message: impl Into<String>) -> Status {
Status::new(Code::Ok, message)
}
pub fn cancelled(message: impl Into<String>) -> Status {
Status::new(Code::Cancelled, message)
}
pub fn unknown(message: impl Into<String>) -> Status {
Status::new(Code::Unknown, message)
}
pub fn invalid_argument(message: impl Into<String>) -> Status {
Status::new(Code::InvalidArgument, message)
}
pub fn deadline_exceeded(message: impl Into<String>) -> Status {
Status::new(Code::DeadlineExceeded, message)
}
pub fn not_found(message: impl Into<String>) -> Status {
Status::new(Code::NotFound, message)
}
pub fn already_exists(message: impl Into<String>) -> Status {
Status::new(Code::AlreadyExists, message)
}
pub fn permission_denied(message: impl Into<String>) -> Status {
Status::new(Code::PermissionDenied, message)
}
pub fn resource_exhausted(message: impl Into<String>) -> Status {
Status::new(Code::ResourceExhausted, message)
}
pub fn failed_precondition(message: impl Into<String>) -> Status {
Status::new(Code::FailedPrecondition, message)
}
pub fn aborted(message: impl Into<String>) -> Status {
Status::new(Code::Aborted, message)
}
pub fn out_of_range(message: impl Into<String>) -> Status {
Status::new(Code::OutOfRange, message)
}
pub fn unimplemented(message: impl Into<String>) -> Status {
Status::new(Code::Unimplemented, message)
}
pub fn internal(message: impl Into<String>) -> Status {
Status::new(Code::Internal, message)
}
pub fn unavailable(message: impl Into<String>) -> Status {
Status::new(Code::Unavailable, message)
}
pub fn data_loss(message: impl Into<String>) -> Status {
Status::new(Code::DataLoss, message)
}
pub fn unauthenticated(message: impl Into<String>) -> Status {
Status::new(Code::Unauthenticated, message)
}
pub(crate) fn from_error_generic(
err: impl Into<Box<dyn Error + Send + Sync + 'static>>,
) -> Status {
Self::from_error(err.into())
}
pub fn from_error(err: Box<dyn Error + Send + Sync + 'static>) -> Status {
Status::try_from_error(err).unwrap_or_else(|err| {
let mut status = Status::new(Code::Unknown, err.to_string());
status.0.source = Some(err.into());
status
})
}
pub fn try_from_error(
err: Box<dyn Error + Send + Sync + 'static>,
) -> Result<Status, Box<dyn Error + Send + Sync + 'static>> {
let err = match err.downcast::<Status>() {
Ok(status) => {
return Ok(*status);
}
Err(err) => err,
};
#[cfg(feature = "server")]
let err = match err.downcast::<h2::Error>() {
Ok(h2) => {
return Ok(Status::from_h2_error(h2));
}
Err(err) => err,
};
#[cfg(feature = "server")]
let err = match err.downcast::<tower::load_shed::error::Overloaded>() {
Ok(_) => {
return Ok(Status::resource_exhausted(
"Too many active requests for the connection",
));
}
Err(err) => err,
};
if let Some(mut status) = find_status_in_source_chain(&*err) {
status.0.source = Some(err.into());
return Ok(status);
}
Err(err)
}
#[cfg(feature = "server")]
fn from_h2_error(err: Box<h2::Error>) -> Status {
let code = Self::code_from_h2(&err);
let mut status = Self::new(code, format!("h2 protocol error: {err}"));
status.0.source = Some(Arc::new(*err));
status
}
#[cfg(feature = "server")]
fn code_from_h2(err: &h2::Error) -> Code {
match err.reason() {
Some(h2::Reason::NO_ERROR)
| Some(h2::Reason::PROTOCOL_ERROR)
| Some(h2::Reason::INTERNAL_ERROR)
| Some(h2::Reason::FLOW_CONTROL_ERROR)
| Some(h2::Reason::SETTINGS_TIMEOUT)
| Some(h2::Reason::FRAME_SIZE_ERROR)
| Some(h2::Reason::COMPRESSION_ERROR)
| Some(h2::Reason::CONNECT_ERROR) => Code::Internal,
Some(h2::Reason::REFUSED_STREAM) => Code::Unavailable,
Some(h2::Reason::CANCEL) => Code::Cancelled,
Some(h2::Reason::ENHANCE_YOUR_CALM) => Code::ResourceExhausted,
Some(h2::Reason::INADEQUATE_SECURITY) => Code::PermissionDenied,
_ => Code::Unknown,
}
}
#[cfg(feature = "server")]
fn to_h2_error(&self) -> h2::Error {
let reason = match self.code() {
Code::Cancelled => h2::Reason::CANCEL,
_ => h2::Reason::INTERNAL_ERROR,
};
reason.into()
}
#[cfg(any(feature = "server", feature = "channel"))]
fn from_hyper_error(err: &hyper::Error) -> Option<Status> {
if err.is_timeout() {
return Some(Status::unavailable(err.to_string()));
}
if err.is_canceled() {
return Some(Status::cancelled(err.to_string()));
}
#[cfg(feature = "server")]
if let Some(h2_err) = err.source().and_then(|e| e.downcast_ref::<h2::Error>()) {
let code = Status::code_from_h2(h2_err);
let status = Self::new(code, format!("h2 protocol error: {err}"));
return Some(status);
}
None
}
pub(crate) fn map_error<E>(err: E) -> Status
where
E: Into<Box<dyn Error + Send + Sync>>,
{
let err: Box<dyn Error + Send + Sync> = err.into();
Status::from_error(err)
}
pub fn from_header_map(header_map: &HeaderMap) -> Option<Status> {
let code = Code::from_bytes(header_map.get(Self::GRPC_STATUS)?.as_ref());
let error_message = match header_map.get(Self::GRPC_MESSAGE) {
Some(header) => percent_decode(header.as_bytes())
.decode_utf8()
.map(|cow| cow.to_string()),
None => Ok(String::new()),
};
let details = match header_map.get(Self::GRPC_STATUS_DETAILS) {
Some(header) => crate::util::base64::STANDARD
.decode(header.as_bytes())
.expect("Invalid status header, expected base64 encoded value")
.into(),
None => Bytes::new(),
};
let other_headers = {
let mut header_map = header_map.clone();
header_map.remove(Self::GRPC_STATUS);
header_map.remove(Self::GRPC_MESSAGE);
header_map.remove(Self::GRPC_STATUS_DETAILS);
header_map
};
let (code, message) = match error_message {
Ok(message) => (code, message),
Err(e) => {
let error_message = format!("Error deserializing status message header: {e}");
warn!(error_message);
(Code::Unknown, error_message)
}
};
Some(
StatusInner {
code,
message,
details,
metadata: MetadataMap::from_headers(other_headers),
source: None,
}
.into_status(),
)
}
pub fn code(&self) -> Code {
self.0.code
}
pub fn message(&self) -> &str {
&self.0.message
}
pub fn details(&self) -> &[u8] {
&self.0.details
}
pub fn metadata(&self) -> &MetadataMap {
&self.0.metadata
}
pub fn metadata_mut(&mut self) -> &mut MetadataMap {
&mut self.0.metadata
}
pub(crate) fn to_header_map(&self) -> Result<HeaderMap, Self> {
let mut header_map = HeaderMap::with_capacity(3 + self.0.metadata.len());
self.add_header(&mut header_map)?;
Ok(header_map)
}
pub fn add_header(&self, header_map: &mut HeaderMap) -> Result<(), Self> {
header_map.extend(self.0.metadata.clone().into_sanitized_headers());
header_map.insert(Self::GRPC_STATUS, self.0.code.to_header_value());
if !self.0.message.is_empty() {
let to_write = Bytes::copy_from_slice(
Cow::from(percent_encode(self.message().as_bytes(), ENCODING_SET)).as_bytes(),
);
header_map.insert(
Self::GRPC_MESSAGE,
HeaderValue::from_maybe_shared(to_write).map_err(invalid_header_value_byte)?,
);
}
if !self.0.details.is_empty() {
let details = crate::util::base64::STANDARD_NO_PAD.encode(&self.0.details[..]);
header_map.insert(
Self::GRPC_STATUS_DETAILS,
HeaderValue::from_maybe_shared(details).map_err(invalid_header_value_byte)?,
);
}
Ok(())
}
pub fn with_details(code: Code, message: impl Into<String>, details: Bytes) -> Status {
Self::with_details_and_metadata(code, message, details, MetadataMap::new())
}
pub fn with_metadata(code: Code, message: impl Into<String>, metadata: MetadataMap) -> Status {
Self::with_details_and_metadata(code, message, Bytes::new(), metadata)
}
pub fn with_details_and_metadata(
code: Code,
message: impl Into<String>,
details: Bytes,
metadata: MetadataMap,
) -> Status {
StatusInner {
code,
message: message.into(),
details,
metadata,
source: None,
}
.into_status()
}
pub fn set_source(&mut self, source: Arc<dyn Error + Send + Sync + 'static>) -> &mut Status {
self.0.source = Some(source);
self
}
pub fn into_http<B: Default>(self) -> http::Response<B> {
let mut response = http::Response::new(B::default());
response
.headers_mut()
.insert(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE);
self.add_header(response.headers_mut()).unwrap();
response.extensions_mut().insert(self);
response
}
#[doc(hidden)]
pub const GRPC_STATUS: HeaderName = HeaderName::from_static("grpc-status");
#[doc(hidden)]
pub const GRPC_MESSAGE: HeaderName = HeaderName::from_static("grpc-message");
#[doc(hidden)]
pub const GRPC_STATUS_DETAILS: HeaderName = HeaderName::from_static("grpc-status-details-bin");
}
fn find_status_in_source_chain(err: &(dyn Error + 'static)) -> Option<Status> {
let mut source = Some(err);
while let Some(err) = source {
if let Some(status) = err.downcast_ref::<Status>() {
return Some(
StatusInner {
code: status.0.code,
message: status.0.message.clone(),
details: status.0.details.clone(),
metadata: status.0.metadata.clone(),
source: None,
}
.into_status(),
);
}
if let Some(timeout) = err.downcast_ref::<TimeoutExpired>() {
return Some(Status::cancelled(timeout.to_string()));
}
if let Some(connect) = err.downcast_ref::<ConnectError>() {
return Some(Status::unavailable(connect.to_string()));
}
#[cfg(any(feature = "server", feature = "channel"))]
if let Some(hyper) = err
.downcast_ref::<hyper::Error>()
.and_then(Status::from_hyper_error)
{
return Some(hyper);
}
source = err.source();
}
None
}
impl fmt::Debug for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl fmt::Debug for StatusInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("Status");
builder.field("code", &self.code);
if !self.message.is_empty() {
builder.field("message", &self.message);
}
if !self.details.is_empty() {
builder.field("details", &self.details);
}
if !self.metadata.is_empty() {
builder.field("metadata", &self.metadata);
}
builder.field("source", &self.source);
builder.finish()
}
}
fn invalid_header_value_byte<Error: fmt::Display>(err: Error) -> Status {
debug!("Invalid header: {}", err);
Status::new(
Code::Internal,
"Couldn't serialize non-text grpc status header".to_string(),
)
}
#[cfg(feature = "server")]
impl From<h2::Error> for Status {
fn from(err: h2::Error) -> Self {
Status::from_h2_error(Box::new(err))
}
}
#[cfg(feature = "server")]
impl From<Status> for h2::Error {
fn from(status: Status) -> Self {
status.to_h2_error()
}
}
impl From<std::io::Error> for Status {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind;
let code = match err.kind() {
ErrorKind::BrokenPipe
| ErrorKind::WouldBlock
| ErrorKind::WriteZero
| ErrorKind::Interrupted => Code::Internal,
ErrorKind::ConnectionRefused
| ErrorKind::ConnectionReset
| ErrorKind::NotConnected
| ErrorKind::AddrInUse
| ErrorKind::AddrNotAvailable => Code::Unavailable,
ErrorKind::AlreadyExists => Code::AlreadyExists,
ErrorKind::ConnectionAborted => Code::Aborted,
ErrorKind::InvalidData => Code::DataLoss,
ErrorKind::InvalidInput => Code::InvalidArgument,
ErrorKind::NotFound => Code::NotFound,
ErrorKind::PermissionDenied => Code::PermissionDenied,
ErrorKind::TimedOut => Code::DeadlineExceeded,
ErrorKind::UnexpectedEof => Code::OutOfRange,
_ => Code::Unknown,
};
Status::new(code, err.to_string())
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "code: '{}'", self.code())?;
if !self.message().is_empty() {
write!(f, ", message: {:?}", self.message())?;
}
if let Some(source) = self.source() {
write!(f, ", source: {source:?}")?;
}
Ok(())
}
}
impl Error for Status {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.0.source.as_ref().map(|err| (&**err) as _)
}
}
pub(crate) fn infer_grpc_status(
trailers: Option<&HeaderMap>,
status_code: http::StatusCode,
) -> Result<(), Option<Status>> {
if let Some(trailers) = trailers
&& let Some(status) = Status::from_header_map(trailers)
{
if status.code() == Code::Ok {
return Ok(());
} else {
return Err(status.into());
}
}
trace!("trailers missing grpc-status");
let code = match status_code {
http::StatusCode::BAD_REQUEST => Code::Internal,
http::StatusCode::UNAUTHORIZED => Code::Unauthenticated,
http::StatusCode::FORBIDDEN => Code::PermissionDenied,
http::StatusCode::NOT_FOUND => Code::Unimplemented,
http::StatusCode::TOO_MANY_REQUESTS
| http::StatusCode::BAD_GATEWAY
| http::StatusCode::SERVICE_UNAVAILABLE
| http::StatusCode::GATEWAY_TIMEOUT => Code::Unavailable,
http::StatusCode::OK => {
return Err(Some(Status::unknown(
"protocol error: missing grpc-status trailer, stream was terminated without a final status (possible truncation by a proxy or load balancer)",
)));
}
_ => Code::Unknown,
};
let msg = format!(
"grpc-status header missing, mapped from HTTP status code {}",
status_code.as_u16(),
);
let status = Status::new(code, msg);
Err(status.into())
}
impl Code {
pub const fn from_i32(i: i32) -> Code {
match i {
0 => Code::Ok,
1 => Code::Cancelled,
2 => Code::Unknown,
3 => Code::InvalidArgument,
4 => Code::DeadlineExceeded,
5 => Code::NotFound,
6 => Code::AlreadyExists,
7 => Code::PermissionDenied,
8 => Code::ResourceExhausted,
9 => Code::FailedPrecondition,
10 => Code::Aborted,
11 => Code::OutOfRange,
12 => Code::Unimplemented,
13 => Code::Internal,
14 => Code::Unavailable,
15 => Code::DataLoss,
16 => Code::Unauthenticated,
_ => Code::Unknown,
}
}
pub fn from_bytes(bytes: &[u8]) -> Code {
match bytes.len() {
1 => match bytes[0] {
b'0' => Code::Ok,
b'1' => Code::Cancelled,
b'2' => Code::Unknown,
b'3' => Code::InvalidArgument,
b'4' => Code::DeadlineExceeded,
b'5' => Code::NotFound,
b'6' => Code::AlreadyExists,
b'7' => Code::PermissionDenied,
b'8' => Code::ResourceExhausted,
b'9' => Code::FailedPrecondition,
_ => Code::parse_err(),
},
2 => match (bytes[0], bytes[1]) {
(b'1', b'0') => Code::Aborted,
(b'1', b'1') => Code::OutOfRange,
(b'1', b'2') => Code::Unimplemented,
(b'1', b'3') => Code::Internal,
(b'1', b'4') => Code::Unavailable,
(b'1', b'5') => Code::DataLoss,
(b'1', b'6') => Code::Unauthenticated,
_ => Code::parse_err(),
},
_ => Code::parse_err(),
}
}
fn to_header_value(self) -> HeaderValue {
match self {
Code::Ok => HeaderValue::from_static("0"),
Code::Cancelled => HeaderValue::from_static("1"),
Code::Unknown => HeaderValue::from_static("2"),
Code::InvalidArgument => HeaderValue::from_static("3"),
Code::DeadlineExceeded => HeaderValue::from_static("4"),
Code::NotFound => HeaderValue::from_static("5"),
Code::AlreadyExists => HeaderValue::from_static("6"),
Code::PermissionDenied => HeaderValue::from_static("7"),
Code::ResourceExhausted => HeaderValue::from_static("8"),
Code::FailedPrecondition => HeaderValue::from_static("9"),
Code::Aborted => HeaderValue::from_static("10"),
Code::OutOfRange => HeaderValue::from_static("11"),
Code::Unimplemented => HeaderValue::from_static("12"),
Code::Internal => HeaderValue::from_static("13"),
Code::Unavailable => HeaderValue::from_static("14"),
Code::DataLoss => HeaderValue::from_static("15"),
Code::Unauthenticated => HeaderValue::from_static("16"),
}
}
fn parse_err() -> Code {
trace!("error parsing grpc-status");
Code::Unknown
}
}
impl From<i32> for Code {
fn from(i: i32) -> Self {
Code::from_i32(i)
}
}
impl From<Code> for i32 {
#[inline]
fn from(code: Code) -> i32 {
code as i32
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BoxError;
#[derive(Debug)]
struct Nested(BoxError);
impl fmt::Display for Nested {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "nested error: {}", self.0)
}
}
impl std::error::Error for Nested {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.0)
}
}
#[test]
fn from_error_status() {
let orig = Status::new(Code::OutOfRange, "weeaboo");
let found = Status::from_error(Box::new(orig));
assert_eq!(found.code(), Code::OutOfRange);
assert_eq!(found.message(), "weeaboo");
}
#[test]
fn from_error_unknown() {
let orig: BoxError = "peek-a-boo".into();
let found = Status::from_error(orig);
assert_eq!(found.code(), Code::Unknown);
assert_eq!(found.message(), "peek-a-boo".to_string());
}
#[test]
fn from_error_nested() {
let orig = Nested(Box::new(Status::new(Code::OutOfRange, "weeaboo")));
let found = Status::from_error(Box::new(orig));
assert_eq!(found.code(), Code::OutOfRange);
assert_eq!(found.message(), "weeaboo");
}
#[test]
#[cfg(feature = "server")]
fn from_error_h2() {
use std::error::Error as _;
let orig = h2::Error::from(h2::Reason::CANCEL);
let found = Status::from_error(Box::new(orig));
assert_eq!(found.code(), Code::Cancelled);
let source = found
.source()
.and_then(|err| err.downcast_ref::<h2::Error>())
.unwrap();
assert_eq!(source.reason(), Some(h2::Reason::CANCEL));
}
#[test]
#[cfg(feature = "server")]
fn code_from_h2_frame_size_error() {
let err = h2::Error::from(h2::Reason::FRAME_SIZE_ERROR);
assert_eq!(Status::code_from_h2(&err), Code::Internal);
}
#[test]
#[cfg(feature = "server")]
fn to_h2_error() {
let orig = Status::new(Code::Cancelled, "stop eet!");
let err = orig.to_h2_error();
assert_eq!(err.reason(), Some(h2::Reason::CANCEL));
}
#[test]
fn code_from_i32() {
for i in 0..(Code::Unauthenticated as i32) {
let code = Code::from(i);
assert_eq!(
i, code as i32,
"Code::from({}) returned {:?} which is {}",
i, code, code as i32,
);
}
assert_eq!(Code::from(-1), Code::Unknown);
}
#[test]
fn constructors() {
assert_eq!(Status::ok("").code(), Code::Ok);
assert_eq!(Status::cancelled("").code(), Code::Cancelled);
assert_eq!(Status::unknown("").code(), Code::Unknown);
assert_eq!(Status::invalid_argument("").code(), Code::InvalidArgument);
assert_eq!(Status::deadline_exceeded("").code(), Code::DeadlineExceeded);
assert_eq!(Status::not_found("").code(), Code::NotFound);
assert_eq!(Status::already_exists("").code(), Code::AlreadyExists);
assert_eq!(Status::permission_denied("").code(), Code::PermissionDenied);
assert_eq!(
Status::resource_exhausted("").code(),
Code::ResourceExhausted
);
assert_eq!(
Status::failed_precondition("").code(),
Code::FailedPrecondition
);
assert_eq!(Status::aborted("").code(), Code::Aborted);
assert_eq!(Status::out_of_range("").code(), Code::OutOfRange);
assert_eq!(Status::unimplemented("").code(), Code::Unimplemented);
assert_eq!(Status::internal("").code(), Code::Internal);
assert_eq!(Status::unavailable("").code(), Code::Unavailable);
assert_eq!(Status::data_loss("").code(), Code::DataLoss);
assert_eq!(Status::unauthenticated("").code(), Code::Unauthenticated);
}
#[test]
fn details() {
const DETAILS: &[u8] = &[0, 2, 3];
let status = Status::with_details(Code::Unavailable, "some message", DETAILS.into());
assert_eq!(status.details(), DETAILS);
let header_map = status.to_header_map().unwrap();
let b64_details = crate::util::base64::STANDARD_NO_PAD.encode(DETAILS);
assert_eq!(header_map[Status::GRPC_STATUS_DETAILS], b64_details);
let status = Status::from_header_map(&header_map).unwrap();
assert_eq!(status.details(), DETAILS);
}
}
#[derive(Debug)]
pub struct TimeoutExpired(pub ());
impl fmt::Display for TimeoutExpired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Timeout expired")
}
}
impl std::error::Error for TimeoutExpired {}
#[derive(Debug)]
pub struct ConnectError(pub Box<dyn std::error::Error + Send + Sync>);
impl fmt::Display for ConnectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_ref())
}
}