Skip to main content

reifydb_sub_server_http/
error.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4//! HTTP error handling and response formatting.
5//!
6//! This module provides error types that implement Axum's `IntoResponse` trait
7//! for consistent error responses across all HTTP endpoints.
8
9use axum::{
10	Json,
11	http::StatusCode,
12	response::{IntoResponse, Response},
13};
14use reifydb_sub_server::{auth::AuthError, execute::ExecuteError};
15use reifydb_type::error::diagnostic::Diagnostic;
16use serde::Serialize;
17
18/// JSON error response body.
19#[derive(Debug, Serialize)]
20pub struct ErrorResponse {
21	/// Human-readable error message.
22	pub error: String,
23	/// Machine-readable error code.
24	pub code: String,
25}
26
27impl ErrorResponse {
28	pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
29		Self {
30			code: code.into(),
31			error: error.into(),
32		}
33	}
34}
35
36/// JSON diagnostic error response body (matches WS format).
37#[derive(Debug, Serialize)]
38pub struct DiagnosticResponse {
39	/// Full diagnostic information.
40	pub diagnostic: Diagnostic,
41}
42
43/// Application error type that converts to HTTP responses.
44#[derive(Debug)]
45pub enum AppError {
46	/// Authentication error.
47	Auth(AuthError),
48	/// Query/command execution error.
49	Execute(ExecuteError),
50	/// Request parsing error.
51	BadRequest(String),
52	/// Internal server error.
53	Internal(String),
54}
55
56impl From<AuthError> for AppError {
57	fn from(e: AuthError) -> Self {
58		AppError::Auth(e)
59	}
60}
61
62impl From<ExecuteError> for AppError {
63	fn from(e: ExecuteError) -> Self {
64		AppError::Execute(e)
65	}
66}
67
68impl std::fmt::Display for AppError {
69	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70		match self {
71			AppError::Auth(e) => write!(f, "Authentication error: {}", e),
72			AppError::Execute(e) => write!(f, "Execution error: {}", e),
73			AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
74			AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
75		}
76	}
77}
78
79impl std::error::Error for AppError {}
80
81impl IntoResponse for AppError {
82	fn into_response(self) -> Response {
83		// Handle engine errors specially - they have full diagnostic info
84		if let AppError::Execute(ExecuteError::Engine {
85			diagnostic,
86			statement,
87		}) = self
88		{
89			tracing::debug!("Engine error: {}", diagnostic.message);
90			// Clone the diagnostic and attach the statement
91			let mut diag = (*diagnostic).clone();
92			if diag.statement.is_none() && !statement.is_empty() {
93				diag.with_statement(statement);
94			}
95			let body = Json(DiagnosticResponse {
96				diagnostic: diag,
97			});
98			return (StatusCode::BAD_REQUEST, body).into_response();
99		}
100
101		let (status, code, message) = match &self {
102			AppError::Auth(AuthError::MissingCredentials) => {
103				(StatusCode::UNAUTHORIZED, "AUTH_REQUIRED", "Authentication required")
104			}
105			AppError::Auth(AuthError::InvalidToken) => {
106				(StatusCode::UNAUTHORIZED, "INVALID_TOKEN", "Invalid authentication token")
107			}
108			AppError::Auth(AuthError::Expired) => {
109				(StatusCode::UNAUTHORIZED, "TOKEN_EXPIRED", "Authentication token expired")
110			}
111			AppError::Auth(AuthError::InvalidHeader) => {
112				(StatusCode::BAD_REQUEST, "INVALID_HEADER", "Malformed authorization header")
113			}
114			AppError::Auth(AuthError::InsufficientPermissions) => {
115				(StatusCode::FORBIDDEN, "FORBIDDEN", "Insufficient permissions for this operation")
116			}
117			AppError::Execute(ExecuteError::Timeout) => {
118				(StatusCode::GATEWAY_TIMEOUT, "QUERY_TIMEOUT", "Query execution timed out")
119			}
120			AppError::Execute(ExecuteError::Cancelled) => {
121				(StatusCode::BAD_REQUEST, "QUERY_CANCELLED", "Query was cancelled")
122			}
123			AppError::Execute(ExecuteError::Disconnected) => {
124				tracing::error!("Query stream disconnected unexpectedly");
125				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
126			}
127			AppError::Execute(ExecuteError::Engine {
128				..
129			}) => {
130				// Already handled above
131				unreachable!()
132			}
133			AppError::BadRequest(msg) => {
134				let body = Json(ErrorResponse::new("BAD_REQUEST", msg.clone()));
135				return (StatusCode::BAD_REQUEST, body).into_response();
136			}
137			AppError::Internal(msg) => {
138				tracing::error!("Internal error: {}", msg);
139				(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Internal server error")
140			}
141		};
142
143		let body = Json(ErrorResponse::new(code, message));
144		(status, body).into_response()
145	}
146}
147
148#[cfg(test)]
149pub mod tests {
150	use super::*;
151
152	#[test]
153	fn test_error_response_serialization() {
154		let resp = ErrorResponse::new("TEST_CODE", "Test error message");
155		let json = serde_json::to_string(&resp).unwrap();
156		assert!(json.contains("TEST_CODE"));
157		assert!(json.contains("Test error message"));
158	}
159
160	#[test]
161	fn test_app_error_display() {
162		let err = AppError::BadRequest("Invalid JSON".to_string());
163		assert_eq!(err.to_string(), "Bad request: Invalid JSON");
164	}
165}