Files
tonic/tonic/src/status.rs
T

708 lines
24 KiB
Rust

use bytes::Bytes;
use http::header::{HeaderMap, HeaderValue};
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
use std::{borrow::Cow, error::Error, fmt};
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'}');
const GRPC_STATUS_HEADER_CODE: &str = "grpc-status";
const GRPC_STATUS_MESSAGE_HEADER: &str = "grpc-message";
const GRPC_STATUS_DETAILS_HEADER: &str = "grpc-status-details-bin";
/// A gRPC status describing the result of an RPC call.
///
/// Values can be created using the `new` function or one of the specialized
/// associated functions.
/// ```rust
/// # use tonic::{Status, Code};
/// let status1 = Status::new(Code::InvalidArgument, "name is invalid");
/// let status2 = Status::invalid_argument("name is invalid");
///
/// assert_eq!(status1.code(), Code::InvalidArgument);
/// assert_eq!(status1.code(), status2.code());
/// ```
#[derive(Clone)]
pub struct Status {
/// The gRPC status code, found in the `grpc-status` header.
code: Code,
/// A relevant error message, found in the `grpc-message` header.
message: String,
/// Binary opaque details, found in the `grpc-status-details-bin` header.
details: Bytes,
}
/// gRPC status codes used by [`Status`].
///
/// These variants match the [gRPC status codes].
///
/// [gRPC status codes]: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Code {
/// The operation completed successfully.
Ok = 0,
/// The operation was cancelled.
Cancelled = 1,
/// Unknown error.
Unknown = 2,
/// Client specified an invalid argument.
InvalidArgument = 3,
/// Deadline expired before operation could complete.
DeadlineExceeded = 4,
/// Some requested entity was not found.
NotFound = 5,
/// Some entity that we attempted to create already exists.
AlreadyExists = 6,
/// The caller does not have permission to execute the specified operation.
PermissionDenied = 7,
/// Some resource has been exhausted.
ResourceExhausted = 8,
/// The system is not in a state required for the operation's execution.
FailedPrecondition = 9,
/// The operation was aborted.
Aborted = 10,
/// Operation was attempted past the valid range.
OutOfRange = 11,
/// Operation is not implemented or not supported.
Unimplemented = 12,
/// Internal error.
Internal = 13,
/// The service is currently unavailable.
Unavailable = 14,
/// Unrecoverable data loss or corruption.
DataLoss = 15,
/// The request does not have valid authentication credentials
Unauthenticated = 16,
// New Codes may be added in the future, so never exhaustively match!
#[doc(hidden)]
__NonExhaustive,
}
// ===== impl Status =====
impl Status {
/// Create a new `Status` with the associated code and message.
pub fn new(code: Code, message: impl Into<String>) -> Status {
Status {
code,
message: message.into(),
details: Bytes::new(),
}
}
/// The operation completed successfully.
pub fn ok(message: impl Into<String>) -> Status {
Status::new(Code::Ok, message)
}
/// The operation was cancelled (typically by the caller).
pub fn cancelled(message: impl Into<String>) -> Status {
Status::new(Code::Cancelled, message)
}
/// Unknown error. An example of where this error may be returned is if a
/// `Status` value received from another address space belongs to an error-space
/// that is not known in this address space. Also errors raised by APIs that
/// do not return enough error information may be converted to this error.
pub fn unknown(message: impl Into<String>) -> Status {
Status::new(Code::Unknown, message)
}
/// Client specified an invalid argument. Note that this differs from
/// `FailedPrecondition`. `InvalidArgument` indicates arguments that are
/// problematic regardless of the state of the system (e.g., a malformed file
/// name).
pub fn invalid_argument(message: impl Into<String>) -> Status {
Status::new(Code::InvalidArgument, message)
}
/// Deadline expired before operation could complete. For operations that
/// change the state of the system, this error may be returned even if the
/// operation has completed successfully. For example, a successful response
/// from a server could have been delayed long enough for the deadline to
/// expire.
pub fn deadline_exceeded(message: impl Into<String>) -> Status {
Status::new(Code::DeadlineExceeded, message)
}
/// Some requested entity (e.g., file or directory) was not found.
pub fn not_found(message: impl Into<String>) -> Status {
Status::new(Code::NotFound, message)
}
/// Some entity that we attempted to create (e.g., file or directory) already
/// exists.
pub fn already_exists(message: impl Into<String>) -> Status {
Status::new(Code::AlreadyExists, message)
}
/// The caller does not have permission to execute the specified operation.
/// `PermissionDenied` must not be used for rejections caused by exhausting
/// some resource (use `ResourceExhausted` instead for those errors).
/// `PermissionDenied` must not be used if the caller cannot be identified
/// (use `Unauthenticated` instead for those errors).
pub fn permission_denied(message: impl Into<String>) -> Status {
Status::new(Code::PermissionDenied, message)
}
/// Some resource has been exhausted, perhaps a per-user quota, or perhaps
/// the entire file system is out of space.
pub fn resource_exhausted(message: impl Into<String>) -> Status {
Status::new(Code::ResourceExhausted, message)
}
/// Operation was rejected because the system is not in a state required for
/// the operation's execution. For example, directory to be deleted may be
/// non-empty, an rmdir operation is applied to a non-directory, etc.
///
/// A litmus test that may help a service implementor in deciding between
/// `FailedPrecondition`, `Aborted`, and `Unavailable`:
/// (a) Use `Unavailable` if the client can retry just the failing call.
/// (b) Use `Aborted` if the client should retry at a higher-level (e.g.,
/// restarting a read-modify-write sequence).
/// (c) Use `FailedPrecondition` if the client should not retry until the
/// system state has been explicitly fixed. E.g., if an "rmdir" fails
/// because the directory is non-empty, `FailedPrecondition` should be
/// returned since the client should not retry unless they have first
/// fixed up the directory by deleting files from it.
pub fn failed_precondition(message: impl Into<String>) -> Status {
Status::new(Code::FailedPrecondition, message)
}
/// The operation was aborted, typically due to a concurrency issue like
/// sequencer check failures, transaction aborts, etc.
///
/// See litmus test above for deciding between `FailedPrecondition`,
/// `Aborted`, and `Unavailable`.
pub fn aborted(message: impl Into<String>) -> Status {
Status::new(Code::Aborted, message)
}
/// Operation was attempted past the valid range. E.g., seeking or reading
/// past end of file.
///
/// Unlike `InvalidArgument`, this error indicates a problem that may be
/// fixed if the system state changes. For example, a 32-bit file system will
/// generate `InvalidArgument if asked to read at an offset that is not in the
/// range [0,2^32-1], but it will generate `OutOfRange` if asked to read from
/// an offset past the current file size.
///
/// There is a fair bit of overlap between `FailedPrecondition` and
/// `OutOfRange`. We recommend using `OutOfRange` (the more specific error)
/// when it applies so that callers who are iterating through a space can
/// easily look for an `OutOfRange` error to detect when they are done.
pub fn out_of_range(message: impl Into<String>) -> Status {
Status::new(Code::OutOfRange, message)
}
/// Operation is not implemented or not supported/enabled in this service.
pub fn unimplemented(message: impl Into<String>) -> Status {
Status::new(Code::Unimplemented, message)
}
/// Internal errors. Means some invariants expected by underlying system has
/// been broken. If you see one of these errors, something is very broken.
pub fn internal(message: impl Into<String>) -> Status {
Status::new(Code::Internal, message)
}
/// The service is currently unavailable. This is a most likely a transient
/// condition and may be corrected by retrying with a back-off.
///
/// See litmus test above for deciding between `FailedPrecondition`,
/// `Aborted`, and `Unavailable`.
pub fn unavailable(message: impl Into<String>) -> Status {
Status::new(Code::Unavailable, message)
}
/// Unrecoverable data loss or corruption.
pub fn data_loss(message: impl Into<String>) -> Status {
Status::new(Code::DataLoss, message)
}
/// The request does not have valid authentication credentials for the
/// operation.
pub fn unauthenticated(message: impl Into<String>) -> Status {
Status::new(Code::Unauthenticated, message)
}
#[cfg_attr(not(feature = "h2"), allow(dead_code))]
pub(crate) fn from_error(err: &(dyn Error + 'static)) -> Status {
Status::try_from_error(err).unwrap_or_else(|| Status::new(Code::Unknown, err.to_string()))
}
fn try_from_error(err: &(dyn Error + 'static)) -> Option<Status> {
let mut cause = Some(err);
while let Some(err) = cause {
if let Some(status) = err.downcast_ref::<Status>() {
return Some(Status {
code: status.code,
message: status.message.clone(),
details: status.details.clone(),
});
}
#[cfg(feature = "h2")]
{
if let Some(h2) = err.downcast_ref::<h2::Error>() {
return Some(Status::from_h2_error(h2));
}
}
cause = err.source();
}
None
}
// FIXME: bubble this into `transport` and expose generic http2 reasons.
#[cfg(feature = "h2")]
fn from_h2_error(err: &h2::Error) -> Status {
// See https://github.com/grpc/grpc/blob/3977c30/doc/PROTOCOL-HTTP2.md#errors
let 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::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,
};
Status::new(code, format!("h2 protocol error: {}", err))
}
#[cfg(feature = "h2")]
fn to_h2_error(&self) -> h2::Error {
// conservatively transform to h2 error codes...
let reason = match self.code {
Code::Cancelled => h2::Reason::CANCEL,
_ => h2::Reason::INTERNAL_ERROR,
};
reason.into()
}
pub(crate) fn map_error<E>(err: E) -> Status
where
E: Into<Box<dyn Error + Send + Sync>>,
{
Status::from_error(&*err.into())
}
pub(crate) fn from_header_map(header_map: &HeaderMap) -> Option<Status> {
header_map.get(GRPC_STATUS_HEADER_CODE).map(|code| {
let code = Code::from_bytes(code.as_ref());
let error_message = header_map
.get(GRPC_STATUS_MESSAGE_HEADER)
.map(|header| {
percent_decode(header.as_bytes())
.decode_utf8()
.map(|cow| cow.to_string())
})
.unwrap_or_else(|| Ok(String::new()));
let details = header_map
.get(GRPC_STATUS_DETAILS_HEADER)
.map(|h| Bytes::copy_from_slice(h.as_bytes()))
.unwrap_or_else(Bytes::new);
match error_message {
Ok(message) => Status {
code,
message,
details,
},
Err(err) => {
warn!("Error deserializing status message header: {}", err);
Status {
code: Code::Unknown,
message: format!("Error deserializing status message header: {}", err),
details,
}
}
}
})
}
/// Get the gRPC `Code` of this `Status`.
pub fn code(&self) -> Code {
self.code
}
/// Get the text error message of this `Status`.
pub fn message(&self) -> &str {
&self.message
}
/// Get the opaque error details of this `Status`.
pub fn details(&self) -> &[u8] {
&self.details
}
pub(crate) fn to_header_map(&self) -> Result<HeaderMap, Self> {
let mut header_map = HeaderMap::with_capacity(3);
self.add_header(&mut header_map)?;
Ok(header_map)
}
pub(crate) fn add_header(&self, header_map: &mut HeaderMap) -> Result<(), Self> {
header_map.insert(GRPC_STATUS_HEADER_CODE, self.code.to_header_value());
if !self.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(
GRPC_STATUS_MESSAGE_HEADER,
HeaderValue::from_maybe_shared(to_write).map_err(invalid_header_value_byte)?,
);
}
if !self.details.is_empty() {
header_map.insert(
GRPC_STATUS_DETAILS_HEADER,
HeaderValue::from_maybe_shared(self.details.clone())
.map_err(invalid_header_value_byte)?,
);
}
Ok(())
}
}
impl fmt::Debug for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// A manual impl to reduce the noise of frequently empty fields.
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);
}
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 = "h2")]
impl From<h2::Error> for Status {
fn from(err: h2::Error) -> Self {
Status::from_h2_error(&err)
}
}
#[cfg(feature = "h2")]
impl From<Status> for h2::Error {
fn from(status: Status) -> Self {
status.to_h2_error()
}
}
impl From<std::io::Error> for Status {
fn from(_io: std::io::Error) -> Self {
unimplemented!()
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"grpc-status: {:?}, grpc-message: {:?}",
self.code(),
self.message()
)
}
}
impl Error for Status {}
///
/// Take the `Status` value from `trailers` if it is available, else from `status_code`.
///
pub(crate) fn infer_grpc_status(
trailers: Option<&HeaderMap>,
status_code: http::StatusCode,
) -> Result<(), Status> {
if let Some(trailers) = trailers {
if let Some(status) = Status::from_header_map(&trailers) {
if status.code() == Code::Ok {
return Ok(());
} else {
return Err(status);
}
}
}
trace!("trailers missing grpc-status");
let code = match status_code {
// Borrowed from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
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,
_ => 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)
}
// ===== impl Code =====
impl Code {
/// Get the `Code` that represents the integer, if known.
///
/// If not known, returns `Code::Unknown` (surprise!).
pub fn from_i32(i: i32) -> Code {
Code::from(i)
}
pub(crate) 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"),
Code::__NonExhaustive => unreachable!("Code::__NonExhaustive"),
}
}
fn parse_err() -> Code {
trace!("error parsing grpc-status");
Code::Unknown
}
}
impl From<i32> for Code {
fn from(i: i32) -> Self {
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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error;
#[derive(Debug)]
struct Nested(Error);
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(&orig);
assert_eq!(orig.code(), found.code());
assert_eq!(orig.message(), found.message());
}
#[test]
fn from_error_unknown() {
let orig: Error = "peek-a-boo".into();
let found = Status::from_error(&*orig);
assert_eq!(found.code(), Code::Unknown);
assert_eq!(found.message(), orig.to_string());
}
#[test]
fn from_error_nested() {
let orig = Nested(Box::new(Status::new(Code::OutOfRange, "weeaboo")));
let found = Status::from_error(&orig);
assert_eq!(found.code(), Code::OutOfRange);
assert_eq!(found.message(), "weeaboo");
}
#[test]
#[cfg(feature = "h2")]
fn from_error_h2() {
let orig = h2::Error::from(h2::Reason::CANCEL);
let found = Status::from_error(&orig);
assert_eq!(found.code(), Code::Cancelled);
}
#[test]
#[cfg(feature = "h2")]
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() {
// This for loop should catch if we ever add a new variant and don't
// update From<i32>.
for i in 0..(Code::__NonExhaustive 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);
assert_eq!(Code::from(Code::__NonExhaustive as i32), 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);
}
}