use anyhow::anyhow; use base64::{engine::general_purpose::STANDARD_NO_PAD as BASE64, Engine}; use bytes::BytesMut; use prost::Message as _; use serde::{Deserialize, Serialize}; use tonic::metadata::MetadataMap; use crate::error::Error; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConnectStatus { /// Connect represents categories of errors as codes. pub code: ConnectCode, /// A user-facing error message. #[serde(default, skip_serializing_if = "String::is_empty")] pub message: String, /// Details are an optional mechanism for servers to attach strongly-typed /// messages to errors. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub details: Vec, } impl ConnectStatus { pub fn new(code: ConnectCode, message: impl Into) -> Self { Self { code, message: message.into(), details: Default::default(), } } pub fn from_connect_response( status_code: http::StatusCode, body: impl AsRef<[u8]>, ) -> Option { let body = body.as_ref(); if !body.is_empty() { match serde_json::from_slice(body) { Ok(status) => return Some(status), Err(err) => { tracing::warn!(?err, "Failed to deserialize error response body"); } } } Some(Self::new(status_code.into(), "")) } pub fn from_tonic_status_lenient(status: &tonic::Status) -> Self { match status.try_into() { Ok(status) => status, Err(err) => { tracing::debug!(?err, "Failed to decode tonic Status details"); Self::new(status.code().into(), status.message()) } } } pub fn into_tonic_status_lenient(self) -> tonic::Status { (&self) .try_into() .unwrap_or_else(|_| tonic::Status::new(self.code.into(), self.message)) } pub fn into_error(self, metadata: MetadataMap) -> Error { let mut status = self.into_tonic_status_lenient(); *status.metadata_mut() = metadata; Error::StatusError(status) } } impl TryFrom for ConnectStatus { type Error = Error; fn try_from(status: tonic_types::pb::Status) -> Result { let details = status.details.iter().map(Into::into).collect(); Ok(Self { code: tonic::Code::from_i32(status.code).into(), message: status.message, details, }) } } impl TryFrom<&ConnectStatus> for tonic_types::pb::Status { type Error = Error; fn try_from(status: &ConnectStatus) -> Result { Ok(tonic_types::pb::Status { code: status.code.into(), message: status.message.clone(), details: status .details .iter() .map(|detail| detail.try_into()) .collect::>()?, }) } } impl TryFrom<&tonic::Status> for ConnectStatus { type Error = Error; fn try_from(status: &tonic::Status) -> Result { let details = status.details(); if details.is_empty() { return Ok(Self::new(status.code().into(), status.message())); } let inner = tonic_types::pb::Status::decode(status.details())?; if inner.code != status.code() as i32 { return Err(Error::InvalidStatus(anyhow!( "details code doesn't match status code" ))); } inner.try_into() } } impl TryFrom<&ConnectStatus> for tonic::Status { type Error = Error; fn try_from(status: &ConnectStatus) -> Result { let details = if !status.details.is_empty() { let proto_status: tonic_types::pb::Status = status.try_into()?; let mut buf = BytesMut::with_capacity(proto_status.encoded_len()); proto_status.encode(&mut buf)?; buf.freeze() } else { Default::default() }; Ok(crate::Status::with_details( status.code.into(), &status.message, details, )) } } /// Connect represents categories of errors as codes. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ConnectCode { /// The operation completed successfully. Ok, /// The operation was cancelled. #[serde(rename = "canceled")] Cancelled, /// Unknown error. Unknown, /// Client specified an invalid argument. InvalidArgument, /// Deadline expired before operation could complete. DeadlineExceeded, /// Some requested entity was not found. NotFound, /// Some entity that we attempted to create already exists. AlreadyExists, /// The caller does not have permission to execute the specified operation. PermissionDenied, /// Some resource has been exhausted. ResourceExhausted, /// The system is not in a state required for the operation's execution. FailedPrecondition, /// The operation was aborted. Aborted, /// Operation was attempted past the valid range. OutOfRange, /// Operation is not implemented or not supported. Unimplemented, /// Internal error. Internal, /// The service is currently unavailable. Unavailable, /// Unrecoverable data loss or corruption. DataLoss, /// The request does not have valid authentication credentials Unauthenticated, } impl From for i32 { fn from(code: ConnectCode) -> Self { tonic::Code::from(code) as i32 } } impl From for ConnectCode { fn from(code: tonic::Code) -> Self { match code { tonic::Code::Ok => Self::Ok, tonic::Code::Cancelled => Self::Cancelled, tonic::Code::Unknown => Self::Unknown, tonic::Code::InvalidArgument => Self::InvalidArgument, tonic::Code::DeadlineExceeded => Self::DeadlineExceeded, tonic::Code::NotFound => Self::NotFound, tonic::Code::AlreadyExists => Self::AlreadyExists, tonic::Code::PermissionDenied => Self::PermissionDenied, tonic::Code::ResourceExhausted => Self::ResourceExhausted, tonic::Code::FailedPrecondition => Self::FailedPrecondition, tonic::Code::Aborted => Self::Aborted, tonic::Code::OutOfRange => Self::OutOfRange, tonic::Code::Unimplemented => Self::Unimplemented, tonic::Code::Internal => Self::Internal, tonic::Code::Unavailable => Self::Unavailable, tonic::Code::DataLoss => Self::DataLoss, tonic::Code::Unauthenticated => Self::Unauthenticated, } } } impl From for tonic::Code { fn from(code: ConnectCode) -> Self { match code { ConnectCode::Ok => Self::Ok, ConnectCode::Cancelled => Self::Cancelled, ConnectCode::Unknown => Self::Unknown, ConnectCode::InvalidArgument => Self::InvalidArgument, ConnectCode::DeadlineExceeded => Self::DeadlineExceeded, ConnectCode::NotFound => Self::NotFound, ConnectCode::AlreadyExists => Self::AlreadyExists, ConnectCode::PermissionDenied => Self::PermissionDenied, ConnectCode::ResourceExhausted => Self::ResourceExhausted, ConnectCode::FailedPrecondition => Self::FailedPrecondition, ConnectCode::Aborted => Self::Aborted, ConnectCode::OutOfRange => Self::OutOfRange, ConnectCode::Unimplemented => Self::Unimplemented, ConnectCode::Internal => Self::Internal, ConnectCode::Unavailable => Self::Unavailable, ConnectCode::DataLoss => Self::DataLoss, ConnectCode::Unauthenticated => Self::Unauthenticated, } } } // https://connectrpc.com/docs/protocol/#http-to-error-code impl From for ConnectCode { fn from(code: http::StatusCode) -> Self { use http::StatusCode; if code.is_success() { return Self::Ok; } match code { StatusCode::BAD_REQUEST => Self::Internal, StatusCode::UNAUTHORIZED => Self::Unauthenticated, StatusCode::FORBIDDEN => Self::PermissionDenied, StatusCode::NOT_FOUND => Self::Unimplemented, StatusCode::TOO_MANY_REQUESTS | StatusCode::BAD_GATEWAY | StatusCode::SERVICE_UNAVAILABLE | StatusCode::GATEWAY_TIMEOUT => Self::Unavailable, _ => Self::Unknown, } } } // https://connectrpc.com/docs/protocol/#error-codes impl From for http::StatusCode { fn from(code: ConnectCode) -> Self { use ConnectCode::*; match code { Ok => Self::OK, Cancelled => Self::from_u16(499).unwrap(), Unknown => Self::INTERNAL_SERVER_ERROR, InvalidArgument => Self::BAD_REQUEST, DeadlineExceeded => Self::GATEWAY_TIMEOUT, NotFound => Self::NOT_FOUND, AlreadyExists => Self::CONFLICT, PermissionDenied => Self::FORBIDDEN, ResourceExhausted => Self::TOO_MANY_REQUESTS, FailedPrecondition => Self::BAD_REQUEST, Aborted => Self::CONFLICT, OutOfRange => Self::BAD_REQUEST, Unimplemented => Self::NOT_IMPLEMENTED, Internal => Self::INTERNAL_SERVER_ERROR, Unavailable => Self::SERVICE_UNAVAILABLE, DataLoss => Self::INTERNAL_SERVER_ERROR, Unauthenticated => Self::UNAUTHORIZED, } } } type JsonObject = serde_json::Map; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConnectStatusDetail { #[serde(rename = "type")] pub message_type: String, #[serde(rename = "value")] value_base64: String, #[serde(default, skip_serializing_if = "JsonObject::is_empty")] pub debug: JsonObject, } impl ConnectStatusDetail { pub fn from_message(message_type: impl Into, message: impl prost::Message) -> Self { let value_bytes = message.encode_to_vec(); let value_b64 = BASE64.encode(&value_bytes); Self { message_type: message_type.into(), value_base64: value_b64, debug: Default::default(), } } pub fn decode_value(&self) -> Result, Error> { Ok(BASE64.decode(&self.value_base64)?) } } impl From<&prost_types::Any> for ConnectStatusDetail { fn from(any: &prost_types::Any) -> Self { let message_type = any.type_url.rsplit('/').next().unwrap().to_string(); let value_base64 = BASE64.encode(&any.value); Self { message_type, value_base64, debug: Default::default(), } } } impl TryFrom<&ConnectStatusDetail> for prost_types::Any { type Error = Error; fn try_from(detail: &ConnectStatusDetail) -> Result { Ok(prost_types::Any { type_url: format!("type.googleapis.com/{}", detail.message_type), value: detail.decode_value()?, }) } }