use bytes::Bytes;
use http::StatusCode;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ErrorCode {
Canceled,
Unknown,
InvalidArgument,
DeadlineExceeded,
NotFound,
AlreadyExists,
PermissionDenied,
ResourceExhausted,
FailedPrecondition,
Aborted,
OutOfRange,
Unimplemented,
Internal,
Unavailable,
DataLoss,
Unauthenticated,
}
impl ErrorCode {
#[inline]
pub fn as_str(&self) -> &'static str {
match self {
Self::Canceled => "canceled",
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",
}
}
#[inline]
pub fn http_status(&self) -> StatusCode {
match self {
Self::Canceled => {
StatusCode::from_u16(499).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
Self::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidArgument => StatusCode::BAD_REQUEST,
Self::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
Self::NotFound => StatusCode::NOT_FOUND,
Self::AlreadyExists => StatusCode::CONFLICT,
Self::PermissionDenied => StatusCode::FORBIDDEN,
Self::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
Self::FailedPrecondition => StatusCode::BAD_REQUEST,
Self::Aborted => StatusCode::CONFLICT,
Self::OutOfRange => StatusCode::BAD_REQUEST,
Self::Unimplemented => StatusCode::NOT_IMPLEMENTED,
Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
Self::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
Self::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
Self::Unauthenticated => StatusCode::UNAUTHORIZED,
}
}
}
impl ErrorCode {
#[inline]
pub fn grpc_code(&self) -> u32 {
match self {
Self::Canceled => 1,
Self::Unknown => 2,
Self::InvalidArgument => 3,
Self::DeadlineExceeded => 4,
Self::NotFound => 5,
Self::AlreadyExists => 6,
Self::PermissionDenied => 7,
Self::ResourceExhausted => 8,
Self::FailedPrecondition => 9,
Self::Aborted => 10,
Self::OutOfRange => 11,
Self::Unimplemented => 12,
Self::Internal => 13,
Self::Unavailable => 14,
Self::DataLoss => 15,
Self::Unauthenticated => 16,
}
}
#[inline]
pub fn from_grpc_code(code: u32) -> Option<Self> {
match code {
1 => Some(Self::Canceled),
2 => Some(Self::Unknown),
3 => Some(Self::InvalidArgument),
4 => Some(Self::DeadlineExceeded),
5 => Some(Self::NotFound),
6 => Some(Self::AlreadyExists),
7 => Some(Self::PermissionDenied),
8 => Some(Self::ResourceExhausted),
9 => Some(Self::FailedPrecondition),
10 => Some(Self::Aborted),
11 => Some(Self::OutOfRange),
12 => Some(Self::Unimplemented),
13 => Some(Self::Internal),
14 => Some(Self::Unavailable),
15 => Some(Self::DataLoss),
16 => Some(Self::Unauthenticated),
_ => None,
}
}
}
impl std::str::FromStr for ErrorCode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"canceled" => Ok(Self::Canceled),
"unknown" => Ok(Self::Unknown),
"invalid_argument" => Ok(Self::InvalidArgument),
"deadline_exceeded" => Ok(Self::DeadlineExceeded),
"not_found" => Ok(Self::NotFound),
"already_exists" => Ok(Self::AlreadyExists),
"permission_denied" => Ok(Self::PermissionDenied),
"resource_exhausted" => Ok(Self::ResourceExhausted),
"failed_precondition" => Ok(Self::FailedPrecondition),
"aborted" => Ok(Self::Aborted),
"out_of_range" => Ok(Self::OutOfRange),
"unimplemented" => Ok(Self::Unimplemented),
"internal" => Ok(Self::Internal),
"unavailable" => Ok(Self::Unavailable),
"data_loss" => Ok(Self::DataLoss),
"unauthenticated" => Ok(Self::Unauthenticated),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetail {
#[serde(rename = "type")]
pub type_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debug: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectError {
pub code: ErrorCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub details: Vec<ErrorDetail>,
#[serde(skip)]
http_status_override: Option<StatusCode>,
#[serde(skip)]
pub response_headers: http::HeaderMap,
#[serde(skip)]
pub trailers: http::HeaderMap,
}
impl ConnectError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: Some(message.into()),
details: Vec::new(),
http_status_override: None,
response_headers: http::HeaderMap::new(),
trailers: http::HeaderMap::new(),
}
}
#[must_use]
pub fn with_headers(mut self, headers: http::HeaderMap) -> Self {
self.response_headers = headers;
self
}
#[must_use]
pub fn with_trailers(mut self, trailers: http::HeaderMap) -> Self {
self.trailers = trailers;
self
}
#[must_use]
pub fn with_http_status(mut self, status: StatusCode) -> Self {
self.http_status_override = Some(status);
self
}
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unknown, message).with_http_status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
}
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unknown, message).with_http_status(StatusCode::METHOD_NOT_ALLOWED)
}
pub fn canceled(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Canceled, message)
}
pub fn unknown(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unknown, message)
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidArgument, message)
}
pub fn deadline_exceeded(message: impl Into<String>) -> Self {
Self::new(ErrorCode::DeadlineExceeded, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorCode::NotFound, message)
}
pub fn already_exists(message: impl Into<String>) -> Self {
Self::new(ErrorCode::AlreadyExists, message)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(ErrorCode::PermissionDenied, message)
}
pub fn resource_exhausted(message: impl Into<String>) -> Self {
Self::new(ErrorCode::ResourceExhausted, message)
}
pub fn failed_precondition(message: impl Into<String>) -> Self {
Self::new(ErrorCode::FailedPrecondition, message)
}
pub fn aborted(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Aborted, message)
}
pub fn out_of_range(message: impl Into<String>) -> Self {
Self::new(ErrorCode::OutOfRange, message)
}
pub fn unimplemented(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unimplemented, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Internal, message)
}
pub fn unavailable(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unavailable, message)
}
pub fn data_loss(message: impl Into<String>) -> Self {
Self::new(ErrorCode::DataLoss, message)
}
pub fn unauthenticated(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unauthenticated, message)
}
#[must_use]
pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
self.details.push(detail);
self
}
pub fn http_status(&self) -> StatusCode {
self.http_status_override
.unwrap_or_else(|| self.code.http_status())
}
pub fn to_json(&self) -> Bytes {
Bytes::from(serde_json::to_vec(self).unwrap_or_else(|_| {
format!(r#"{{"code":"{}"}}"#, self.code.as_str()).into_bytes()
}))
}
}
impl std::fmt::Display for ConnectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code.as_str())?;
if let Some(ref message) = self.message {
write!(f, ": {message}")?;
}
Ok(())
}
}
impl std::error::Error for ConnectError {}
impl From<std::io::Error> for ConnectError {
fn from(err: std::io::Error) -> Self {
Self::internal(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grpc_code_round_trip() {
let codes = [
ErrorCode::Canceled,
ErrorCode::Unknown,
ErrorCode::InvalidArgument,
ErrorCode::DeadlineExceeded,
ErrorCode::NotFound,
ErrorCode::AlreadyExists,
ErrorCode::PermissionDenied,
ErrorCode::ResourceExhausted,
ErrorCode::FailedPrecondition,
ErrorCode::Aborted,
ErrorCode::OutOfRange,
ErrorCode::Unimplemented,
ErrorCode::Internal,
ErrorCode::Unavailable,
ErrorCode::DataLoss,
ErrorCode::Unauthenticated,
];
for code in codes {
let grpc = code.grpc_code();
let back = ErrorCode::from_grpc_code(grpc);
assert_eq!(
back,
Some(code),
"round-trip failed for {code:?} (grpc code {grpc})"
);
}
}
#[test]
fn test_grpc_code_values() {
assert_eq!(ErrorCode::Canceled.grpc_code(), 1);
assert_eq!(ErrorCode::Unknown.grpc_code(), 2);
assert_eq!(ErrorCode::Internal.grpc_code(), 13);
assert_eq!(ErrorCode::Unauthenticated.grpc_code(), 16);
}
#[test]
fn test_from_grpc_code_ok_returns_none() {
assert_eq!(ErrorCode::from_grpc_code(0), None);
}
#[test]
fn test_from_grpc_code_unknown_returns_none() {
assert_eq!(ErrorCode::from_grpc_code(17), None);
assert_eq!(ErrorCode::from_grpc_code(999), None);
}
}