Skip to main content

reifydb_sub_server_http/
error.rs

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