1use axum::Json;
7use axum::http::header::CONTENT_TYPE;
8use axum::http::{HeaderValue, StatusCode};
9use axum::response::{IntoResponse, Response};
10use quiver_core::CoreError;
11use quiver_embed::Error as EngineError;
12use serde_json::json;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum Error {
19 #[error(transparent)]
21 Engine(#[from] EngineError),
22 #[error("{0}")]
25 Forbidden(String),
26 #[error("{0}")]
30 BadRequest(String),
31 #[error("configuration error: {0}")]
33 Config(String),
34 #[error("i/o error: {0}")]
36 Io(#[from] std::io::Error),
37 #[error("internal error: {0}")]
39 Internal(String),
40 #[error("{0}")]
45 Upstream(String),
46}
47
48impl Error {
49 fn category(&self) -> (StatusCode, tonic::Code) {
51 match self {
52 Error::Engine(EngineError::CollectionNotFound(_))
53 | Error::Engine(EngineError::Core(CoreError::NotFound(_))) => {
54 (StatusCode::NOT_FOUND, tonic::Code::NotFound)
55 }
56 Error::Engine(EngineError::Core(CoreError::AlreadyExists(_))) => {
57 (StatusCode::CONFLICT, tonic::Code::AlreadyExists)
58 }
59 Error::Forbidden(_) => (StatusCode::FORBIDDEN, tonic::Code::PermissionDenied),
60 Error::BadRequest(_) => (StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument),
61 Error::Upstream(_) => (StatusCode::BAD_GATEWAY, tonic::Code::Unavailable),
62 Error::Engine(EngineError::Core(CoreError::InvalidArgument(_)))
63 | Error::Engine(EngineError::Index(_))
64 | Error::Engine(EngineError::Unsupported(_))
65 | Error::Engine(EngineError::Json(_)) => {
66 (StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument)
67 }
68 _ => (StatusCode::INTERNAL_SERVER_ERROR, tonic::Code::Internal),
69 }
70 }
71
72 fn client_message(&self) -> String {
74 let (status, _) = self.category();
75 if status.is_server_error() && !matches!(self, Error::Upstream(_)) {
78 "internal error".to_owned()
79 } else {
80 self.to_string()
81 }
82 }
83
84 pub(crate) fn to_status(&self) -> tonic::Status {
86 let (status, code) = self.category();
87 if status.is_server_error() {
88 tracing::error!(error = %self, "request failed");
89 }
90 tonic::Status::new(code, self.client_message())
91 }
92}
93
94impl IntoResponse for Error {
95 fn into_response(self) -> Response {
96 let (status, _) = self.category();
97 if status.is_server_error() {
98 tracing::error!(error = %self, "request failed");
99 }
100 let body = json!({
101 "type": "about:blank",
102 "title": status.canonical_reason().unwrap_or("Error"),
103 "status": status.as_u16(),
104 "detail": self.client_message(),
105 });
106 let mut response = (status, Json(body)).into_response();
107 response.headers_mut().insert(
108 CONTENT_TYPE,
109 HeaderValue::from_static("application/problem+json"),
110 );
111 response
112 }
113}