use axum::Json;
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use quiver_core::CoreError;
use quiver_embed::Error as EngineError;
use serde_json::json;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Engine(#[from] EngineError),
#[error("{0}")]
Forbidden(String),
#[error("{0}")]
BadRequest(String),
#[error("configuration error: {0}")]
Config(String),
#[error("i/o error: {0}")]
Io(#[from] std::io::Error),
#[error("internal error: {0}")]
Internal(String),
#[error("{0}")]
Upstream(String),
}
impl Error {
fn category(&self) -> (StatusCode, tonic::Code) {
match self {
Error::Engine(EngineError::CollectionNotFound(_))
| Error::Engine(EngineError::Core(CoreError::NotFound(_))) => {
(StatusCode::NOT_FOUND, tonic::Code::NotFound)
}
Error::Engine(EngineError::Core(CoreError::AlreadyExists(_))) => {
(StatusCode::CONFLICT, tonic::Code::AlreadyExists)
}
Error::Forbidden(_) => (StatusCode::FORBIDDEN, tonic::Code::PermissionDenied),
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument),
Error::Upstream(_) => (StatusCode::BAD_GATEWAY, tonic::Code::Unavailable),
Error::Engine(EngineError::Core(CoreError::InvalidArgument(_)))
| Error::Engine(EngineError::Index(_))
| Error::Engine(EngineError::Unsupported(_))
| Error::Engine(EngineError::Json(_)) => {
(StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument)
}
_ => (StatusCode::INTERNAL_SERVER_ERROR, tonic::Code::Internal),
}
}
fn client_message(&self) -> String {
let (status, _) = self.category();
if status.is_server_error() && !matches!(self, Error::Upstream(_)) {
"internal error".to_owned()
} else {
self.to_string()
}
}
pub(crate) fn to_status(&self) -> tonic::Status {
let (status, code) = self.category();
if status.is_server_error() {
tracing::error!(error = %self, "request failed");
}
tonic::Status::new(code, self.client_message())
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let (status, _) = self.category();
if status.is_server_error() {
tracing::error!(error = %self, "request failed");
}
let body = json!({
"type": "about:blank",
"title": status.canonical_reason().unwrap_or("Error"),
"status": status.as_u16(),
"detail": self.client_message(),
});
let mut response = (status, Json(body)).into_response();
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/problem+json"),
);
response
}
}