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 #[error("not the raft leader; leader: {leader:?}")]
53 NotLeader {
54 leader: Option<String>,
56 },
57}
58
59impl Error {
60 fn category(&self) -> (StatusCode, tonic::Code) {
62 match self {
63 Error::Engine(EngineError::CollectionNotFound(_))
64 | Error::Engine(EngineError::Core(CoreError::NotFound(_))) => {
65 (StatusCode::NOT_FOUND, tonic::Code::NotFound)
66 }
67 Error::Engine(EngineError::Core(CoreError::AlreadyExists(_))) => {
68 (StatusCode::CONFLICT, tonic::Code::AlreadyExists)
69 }
70 Error::Forbidden(_) => (StatusCode::FORBIDDEN, tonic::Code::PermissionDenied),
71 Error::BadRequest(_) => (StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument),
72 Error::Upstream(_) => (StatusCode::BAD_GATEWAY, tonic::Code::Unavailable),
73 Error::NotLeader { .. } => (StatusCode::MISDIRECTED_REQUEST, tonic::Code::Unavailable),
74 Error::Engine(EngineError::Core(CoreError::InvalidArgument(_)))
75 | Error::Engine(EngineError::Index(_))
76 | Error::Engine(EngineError::Unsupported(_))
77 | Error::Engine(EngineError::Json(_)) => {
78 (StatusCode::BAD_REQUEST, tonic::Code::InvalidArgument)
79 }
80 _ => (StatusCode::INTERNAL_SERVER_ERROR, tonic::Code::Internal),
81 }
82 }
83
84 fn client_message(&self) -> String {
86 let (status, _) = self.category();
87 if status.is_server_error() && !matches!(self, Error::Upstream(_)) {
90 "internal error".to_owned()
91 } else {
92 self.to_string()
93 }
94 }
95
96 pub(crate) fn to_status(&self) -> tonic::Status {
98 let (status, code) = self.category();
99 if status.is_server_error() {
100 tracing::error!(error = %self, "request failed");
101 }
102 tonic::Status::new(code, self.client_message())
103 }
104}
105
106impl IntoResponse for Error {
107 fn into_response(self) -> Response {
108 let (status, _) = self.category();
109 if status.is_server_error() {
110 tracing::error!(error = %self, "request failed");
111 }
112 let body = json!({
113 "type": "about:blank",
114 "title": status.canonical_reason().unwrap_or("Error"),
115 "status": status.as_u16(),
116 "detail": self.client_message(),
117 });
118 let mut response = (status, Json(body)).into_response();
119 response.headers_mut().insert(
120 CONTENT_TYPE,
121 HeaderValue::from_static("application/problem+json"),
122 );
123 response
124 }
125}