use autumn_web::AutumnError;
use http::StatusCode;
use tonic::{Code, Status};
#[must_use]
pub const fn http_status(code: Code) -> StatusCode {
match code {
Code::InvalidArgument | Code::OutOfRange | Code::FailedPrecondition => {
StatusCode::BAD_REQUEST
}
Code::Unauthenticated => StatusCode::UNAUTHORIZED,
Code::PermissionDenied => StatusCode::FORBIDDEN,
Code::NotFound => StatusCode::NOT_FOUND,
Code::AlreadyExists | Code::Aborted => StatusCode::CONFLICT,
Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
Code::Ok
| Code::Cancelled
| Code::Unknown
| Code::Unimplemented
| Code::Internal
| Code::DataLoss => StatusCode::BAD_GATEWAY,
}
}
const fn client_text(code: Code) -> &'static str {
match code {
Code::InvalidArgument => "invalid argument",
Code::OutOfRange => "out of range",
Code::FailedPrecondition => "failed precondition",
Code::Unauthenticated => "unauthenticated",
Code::PermissionDenied => "permission denied",
Code::NotFound => "not found",
Code::AlreadyExists => "already exists",
Code::Aborted => "aborted",
Code::ResourceExhausted => "resource exhausted",
_ => "downstream gRPC call failed",
}
}
#[derive(Debug)]
struct CallFailed(String);
impl std::fmt::Display for CallFailed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for CallFailed {}
#[must_use]
pub fn status_to_error(status: &Status) -> AutumnError {
let code = status.code();
let http = http_status(code);
let name = crate::metrics::code_name(code as i32);
let text = if http.is_server_error() {
tracing::warn!(
grpc_code = name,
message = status.message(),
"downstream gRPC call failed"
);
format!("downstream gRPC call failed: {name}: {}", status.message())
} else {
tracing::debug!(
grpc_code = name,
message = status.message(),
"downstream gRPC call failed"
);
client_text(code).to_owned()
};
AutumnError::from(CallFailed(text)).with_status(http)
}
pub trait GrpcResultExt<T>: sealed::Sealed {
fn or_http(self) -> Result<T, AutumnError>;
}
mod sealed {
pub trait Sealed {}
impl<T> Sealed for Result<T, tonic::Status> {}
}
impl<T> GrpcResultExt<T> for Result<T, Status> {
fn or_http(self) -> Result<T, AutumnError> {
self.map_err(|status| status_to_error(&status))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn every_code_has_a_fixed_status() {
let expected = [
(Code::Ok, 502),
(Code::Cancelled, 502),
(Code::Unknown, 502),
(Code::InvalidArgument, 400),
(Code::DeadlineExceeded, 504),
(Code::NotFound, 404),
(Code::AlreadyExists, 409),
(Code::PermissionDenied, 403),
(Code::ResourceExhausted, 429),
(Code::FailedPrecondition, 400),
(Code::Aborted, 409),
(Code::OutOfRange, 400),
(Code::Unimplemented, 502),
(Code::Internal, 502),
(Code::Unavailable, 503),
(Code::DataLoss, 502),
(Code::Unauthenticated, 401),
];
assert_eq!(expected.len(), 17, "all codes");
for (code, status) in expected {
assert_eq!(http_status(code).as_u16(), status, "{code:?}");
let error = status_to_error(&Status::new(code, "secret"));
assert_eq!(error.status().as_u16(), status, "{code:?}");
let message = error.message();
if status < 500 {
assert!(!message.contains("secret"), "{code:?}: {message}");
} else {
assert!(message.contains("secret"), "{code:?}: {message}");
}
}
}
#[test]
fn or_http_passes_ok_values_through() {
assert_eq!(Ok::<_, Status>(5).or_http().unwrap_or_default(), 5);
let error = Err::<(), _>(Status::not_found("x")).or_http().unwrap_err();
assert_eq!(error.status(), StatusCode::NOT_FOUND);
assert_eq!(error.message(), "not found");
}
}