Skip to main content

reifydb_sub_server_http/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 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_type::error::Diagnostic;
13use serde::Serialize;
14use tracing::{debug, error};
15
16/// JSON error response body.
17#[derive(Debug, Serialize)]
18pub struct ErrorResponse {
19	/// Human-readable error message.
20	pub error: String,
21	/// Machine-readable error code.
22	pub code: String,
23}
24
25impl ErrorResponse {
26	pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
27		Self {
28			code: code.into(),
29			error: error.into(),
30		}
31	}
32}
33
34/// JSON diagnostic error response body (matches WS format).
35#[derive(Debug, Serialize)]
36pub struct DiagnosticResponse {
37	/// Full diagnostic information.
38	pub diagnostic: Diagnostic,
39}
40
41/// Application error type that converts to HTTP responses.
42#[derive(Debug)]
43pub enum AppError {
44	/// Authentication error.
45	Auth(AuthError),
46	/// Query/command execution error.
47	Execute(ExecuteError),
48	/// Request parsing error.
49	BadRequest(String),
50	/// Invalid parameter error.
51	InvalidParams(String),
52	/// Resource not found (404).
53	NotFound(String),
54	/// HTTP method not allowed for this resource (405).
55	MethodNotAllowed(String),
56	/// Internal server error.
57	Internal(String),
58}
59
60impl From<AuthError> for AppError {
61	fn from(e: AuthError) -> Self {
62		AppError::Auth(e)
63	}
64}
65
66impl From<ExecuteError> for AppError {
67	fn from(e: ExecuteError) -> Self {
68		AppError::Execute(e)
69	}
70}
71
72impl fmt::Display for AppError {
73	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74		match self {
75			AppError::Auth(e) => write!(f, "Authentication error: {}", e),
76			AppError::Execute(e) => write!(f, "Execution error: {}", e),
77			AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
78			AppError::InvalidParams(msg) => write!(f, "Invalid params: {}", msg),
79			AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
80			AppError::MethodNotAllowed(msg) => write!(f, "Method not allowed: {}", msg),
81			AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
82		}
83	}
84}
85
86impl error::Error for AppError {}
87
88impl IntoResponse for AppError {
89	fn into_response(self) -> Response {
90		// Handle engine errors specially - they have full diagnostic info
91		if let AppError::Execute(ExecuteError::Engine {
92			diagnostic,
93			rql,
94		}) = self
95		{
96			debug!("Engine error: {}", diagnostic.message);
97			let mut diag = (*diagnostic).clone();
98			if diag.rql.is_none() && !rql.is_empty() {
99				diag.with_rql(rql);
100			}
101			let status = if diag.code.starts_with("POLICY_") {
102				StatusCode::FORBIDDEN
103			} else {
104				StatusCode::BAD_REQUEST
105			};
106			let body = Json(DiagnosticResponse {
107				diagnostic: diag,
108			});
109			return (status, body).into_response();
110		}
111
112		let (status, code, message) = match &self {
113			AppError::Auth(AuthError::MissingCredentials) => {
114				(StatusCode::UNAUTHORIZED, "AUTH_REQUIRED", "Authentication required")
115			}
116			AppError::Auth(AuthError::InvalidToken) => {
117				(StatusCode::UNAUTHORIZED, "INVALID_TOKEN", "Invalid authentication token")
118			}
119			AppError::Auth(AuthError::Expired) => {
120				(StatusCode::UNAUTHORIZED, "TOKEN_EXPIRED", "Authentication token expired")
121			}
122			AppError::Auth(AuthError::InvalidHeader) => {
123				(StatusCode::BAD_REQUEST, "INVALID_HEADER", "Malformed authorization header")
124			}
125			AppError::Auth(AuthError::InsufficientPermissions) => {
126				(StatusCode::FORBIDDEN, "FORBIDDEN", "Insufficient permissions for this operation")
127			}
128			AppError::Execute(ExecuteError::Timeout) => {
129				(StatusCode::GATEWAY_TIMEOUT, "QUERY_TIMEOUT", "Query execution timed out")
130			}
131			AppError::Execute(ExecuteError::Cancelled) => {
132				(StatusCode::BAD_REQUEST, "QUERY_CANCELLED", "Query was cancelled")
133			}
134			AppError::Execute(ExecuteError::Disconnected) => {
135				error!("Query stream disconnected unexpectedly");
136				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
137			}
138			AppError::Execute(ExecuteError::Rejected {
139				code,
140				message,
141			}) => {
142				let body = Json(ErrorResponse::new(code, message));
143				return (StatusCode::FORBIDDEN, body).into_response();
144			}
145			AppError::Execute(ExecuteError::Engine {
146				..
147			}) => {
148				// Already handled above
149				unreachable!()
150			}
151			AppError::BadRequest(msg) => {
152				let body = Json(ErrorResponse::new("BAD_REQUEST", msg.clone()));
153				return (StatusCode::BAD_REQUEST, body).into_response();
154			}
155			AppError::InvalidParams(msg) => {
156				let body = Json(ErrorResponse::new("INVALID_PARAMS", msg.clone()));
157				return (StatusCode::BAD_REQUEST, body).into_response();
158			}
159			AppError::NotFound(msg) => {
160				let body = Json(ErrorResponse::new("NOT_FOUND", msg.clone()));
161				return (StatusCode::NOT_FOUND, body).into_response();
162			}
163			AppError::MethodNotAllowed(msg) => {
164				let body = Json(ErrorResponse::new("METHOD_NOT_ALLOWED", msg.clone()));
165				return (StatusCode::METHOD_NOT_ALLOWED, body).into_response();
166			}
167			AppError::Internal(msg) => {
168				error!("Internal error: {}", msg);
169				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
170			}
171		};
172
173		let body = Json(ErrorResponse::new(code, message));
174		(status, body).into_response()
175	}
176}
177
178#[cfg(test)]
179pub mod tests {
180	use serde_json::to_string;
181
182	use super::*;
183
184	#[test]
185	fn test_error_response_serialization() {
186		let resp = ErrorResponse::new("TEST_CODE", "Test error message");
187		let json = to_string(&resp).unwrap();
188		assert!(json.contains("TEST_CODE"));
189		assert!(json.contains("Test error message"));
190	}
191
192	#[test]
193	fn test_app_error_display() {
194		let err = AppError::BadRequest("Invalid JSON".to_string());
195		assert_eq!(err.to_string(), "Bad request: Invalid JSON");
196	}
197}