Skip to main content

reifydb_sub_server_http/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{error, fmt};
5
6use axum::{
7	Json,
8	http::StatusCode,
9	response::{IntoResponse, Response},
10};
11use reifydb_sub_server::{auth::AuthError, execute::ExecuteError};
12use reifydb_value::error::Diagnostic;
13use serde::Serialize;
14use tracing::{debug, error};
15
16#[derive(Debug, Serialize)]
17pub struct ErrorResponse {
18	pub error: String,
19
20	pub code: String,
21}
22
23impl ErrorResponse {
24	pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
25		Self {
26			code: code.into(),
27			error: error.into(),
28		}
29	}
30}
31
32#[derive(Debug, Serialize)]
33pub struct DiagnosticResponse {
34	pub diagnostic: Diagnostic,
35}
36
37#[derive(Debug)]
38pub enum AppError {
39	Auth(AuthError),
40
41	Execute(ExecuteError),
42
43	BadRequest(String),
44
45	InvalidParams(String),
46
47	NotFound(String),
48
49	MethodNotAllowed(String),
50
51	Internal(String),
52}
53
54impl From<AuthError> for AppError {
55	fn from(e: AuthError) -> Self {
56		AppError::Auth(e)
57	}
58}
59
60impl From<ExecuteError> for AppError {
61	fn from(e: ExecuteError) -> Self {
62		AppError::Execute(e)
63	}
64}
65
66impl fmt::Display for AppError {
67	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68		match self {
69			AppError::Auth(e) => write!(f, "Authentication error: {}", e),
70			AppError::Execute(e) => write!(f, "Execution error: {}", e),
71			AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
72			AppError::InvalidParams(msg) => write!(f, "Invalid params: {}", msg),
73			AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
74			AppError::MethodNotAllowed(msg) => write!(f, "Method not allowed: {}", msg),
75			AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
76		}
77	}
78}
79
80impl error::Error for AppError {}
81
82impl IntoResponse for AppError {
83	fn into_response(self) -> Response {
84		if let AppError::Execute(ExecuteError::Engine {
85			diagnostic,
86			rql,
87		}) = self
88		{
89			debug!("Engine error: {}", diagnostic.message);
90			let mut diag = (*diagnostic).clone();
91			if diag.rql.is_none() && !rql.is_empty() {
92				diag.with_rql(rql);
93			}
94			let status = if diag.code.starts_with("POLICY_") {
95				StatusCode::FORBIDDEN
96			} else {
97				StatusCode::BAD_REQUEST
98			};
99			let body = Json(DiagnosticResponse {
100				diagnostic: diag,
101			});
102			return (status, body).into_response();
103		}
104
105		let (status, code, message) = match &self {
106			AppError::Auth(AuthError::MissingCredentials) => {
107				(StatusCode::UNAUTHORIZED, "AUTH_REQUIRED", "Authentication required")
108			}
109			AppError::Auth(AuthError::InvalidToken) => {
110				(StatusCode::UNAUTHORIZED, "INVALID_TOKEN", "Invalid authentication token")
111			}
112			AppError::Auth(AuthError::Expired) => {
113				(StatusCode::UNAUTHORIZED, "TOKEN_EXPIRED", "Authentication token expired")
114			}
115			AppError::Auth(AuthError::InvalidHeader) => {
116				(StatusCode::BAD_REQUEST, "INVALID_HEADER", "Malformed authorization header")
117			}
118			AppError::Auth(AuthError::InsufficientPermissions) => {
119				(StatusCode::FORBIDDEN, "FORBIDDEN", "Insufficient permissions for this operation")
120			}
121			AppError::Auth(AuthError::Internal) => {
122				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
123			}
124			AppError::Execute(ExecuteError::Timeout) => {
125				(StatusCode::GATEWAY_TIMEOUT, "QUERY_TIMEOUT", "Query execution timed out")
126			}
127			AppError::Execute(ExecuteError::Cancelled) => {
128				(StatusCode::BAD_REQUEST, "QUERY_CANCELLED", "Query was cancelled")
129			}
130			AppError::Execute(ExecuteError::Disconnected) => {
131				error!("Query stream disconnected unexpectedly");
132				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
133			}
134			AppError::Execute(ExecuteError::Rejected {
135				code,
136				message,
137			}) => {
138				let body = Json(ErrorResponse::new(code, message));
139				return (StatusCode::FORBIDDEN, body).into_response();
140			}
141			AppError::Execute(ExecuteError::Engine {
142				..
143			}) => {
144				unreachable!()
145			}
146			AppError::BadRequest(msg) => {
147				let body = Json(ErrorResponse::new("BAD_REQUEST", msg.clone()));
148				return (StatusCode::BAD_REQUEST, body).into_response();
149			}
150			AppError::InvalidParams(msg) => {
151				let body = Json(ErrorResponse::new("INVALID_PARAMS", msg.clone()));
152				return (StatusCode::BAD_REQUEST, body).into_response();
153			}
154			AppError::NotFound(msg) => {
155				let body = Json(ErrorResponse::new("NOT_FOUND", msg.clone()));
156				return (StatusCode::NOT_FOUND, body).into_response();
157			}
158			AppError::MethodNotAllowed(msg) => {
159				let body = Json(ErrorResponse::new("METHOD_NOT_ALLOWED", msg.clone()));
160				return (StatusCode::METHOD_NOT_ALLOWED, body).into_response();
161			}
162			AppError::Internal(msg) => {
163				error!("Internal error: {}", msg);
164				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
165			}
166		};
167
168		let body = Json(ErrorResponse::new(code, message));
169		(status, body).into_response()
170	}
171}
172
173#[cfg(test)]
174pub mod tests {
175	use serde_json::to_string;
176
177	use super::*;
178
179	#[test]
180	fn test_error_response_serialization() {
181		let resp = ErrorResponse::new("TEST_CODE", "Test error message");
182		let json = to_string(&resp).unwrap();
183		assert!(json.contains("TEST_CODE"));
184		assert!(json.contains("Test error message"));
185	}
186
187	#[test]
188	fn test_app_error_display() {
189		let err = AppError::BadRequest("Invalid JSON".to_string());
190		assert_eq!(err.to_string(), "Bad request: Invalid JSON");
191	}
192
193	#[test]
194	fn storage_failure_answers_500_while_a_bad_token_answers_401() {
195		// A 401 tells the client its token is bad, so it retries forever and never sees the real fault.
196		assert_eq!(
197			AppError::Auth(AuthError::InvalidToken).into_response().status(),
198			StatusCode::UNAUTHORIZED,
199			"a token that was never issued is the client's fault"
200		);
201		assert_eq!(
202			AppError::Auth(AuthError::Expired).into_response().status(),
203			StatusCode::UNAUTHORIZED,
204			"an expired token is the client's fault"
205		);
206		assert_eq!(
207			AppError::Auth(AuthError::Internal).into_response().status(),
208			StatusCode::INTERNAL_SERVER_ERROR,
209			"authentication that could not reach storage is the server's fault"
210		);
211	}
212}