Skip to main content

ferrox_errors/
lib.rs

1//! # Ferrox Errors (`ferrox-errors`)
2//!
3//! `ferrox-errors` defines the unified error handling strategy for Ferrox applications. It provides the standard
4//! `AppError` enum and implements Axum's `IntoResponse` trait to automatically convert application errors into
5//! consistent JSON responses with standard HTTP status codes.
6//!
7//! ## Rationale & Design
8//! In enterprise Rust microservices, unhandled errors or raw `Result<T, E>` returns can leak internal stack traces
9//! or produce inconsistent JSON structures for frontend consumers. `ferrox-errors` solves this by forcing all
10//! service layers to emit `AppError`, which serializes into predictable `{ "status": u16, "message": String }` responses.
11//!
12//! ## Key Features
13//! - 🚫 **AppError Enum**: Structured variants for `NotFound`, `ValidationError`, `Unauthorized`, `InternalError`, and `DatabaseError`.
14//! - âš¡ **Axum IntoResponse**: Seamless integration with Axum route handlers without manual status code mappings.
15//! - 🔒 **Security Sanitization**: Internal server errors and database errors are logged privately while safe generic error messages are exposed to clients.
16//!
17//! ## Example Usage
18//! ```rust
19//! use ferrox_errors::{AppError, ErrorResponse};
20//! use axum::response::IntoResponse;
21//!
22//! fn find_user(id: u64) -> Result<String, AppError> {
23//!     if id == 0 {
24//!         Err(AppError::NotFound("User not found".into()))
25//!     } else {
26//!         Ok("Alice".into())
27//!     }
28//! }
29//! ```
30
31use axum::{
32    http::StatusCode,
33    response::{IntoResponse, Response},
34    Json,
35};
36use serde::Serialize;
37use thiserror::Error;
38
39/// A standard global error type for the application.
40#[derive(Debug, Error)]
41pub enum AppError {
42    #[error("Not Found: {0}")]
43    NotFound(String),
44
45    #[error("Validation Error: {0}")]
46    ValidationError(String),
47
48    #[error("Unauthorized: {0}")]
49    Unauthorized(String),
50
51    #[error("Internal Error: {0}")]
52    InternalError(String),
53
54    #[error("Internal Server Error")]
55    InternalServerError(#[source] Box<dyn std::error::Error + Send + Sync>),
56
57    #[error("Database Error: {0}")]
58    DatabaseError(String),
59}
60
61/// Standardized JSON response format for errors
62#[derive(Serialize)]
63pub struct ErrorResponse {
64    pub status: u16,
65    pub message: String,
66}
67
68impl IntoResponse for AppError {
69    fn into_response(self) -> Response {
70        let (status, message) = match &self {
71            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
72            AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
73            AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
74            AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
75            AppError::InternalServerError(err) => {
76                eprintln!("Internal Server Error: {}", err);
77                (
78                    StatusCode::INTERNAL_SERVER_ERROR,
79                    "Internal Server Error".to_string(),
80                )
81            }
82            AppError::DatabaseError(msg) => {
83                eprintln!("Database Error: {}", msg);
84                (
85                    StatusCode::INTERNAL_SERVER_ERROR,
86                    "Database Error".to_string(),
87                )
88            }
89        };
90
91        let body = Json(ErrorResponse {
92            status: status.as_u16(),
93            message,
94        });
95
96        (status, body).into_response()
97    }
98}
99
100pub fn setup() {
101    println!("ferrox-errors initialized: Provides global AppError and IntoResponse for Axum.");
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use axum::response::IntoResponse;
108    use axum::http::StatusCode;
109
110    #[test]
111    fn test_error_formatting() {
112        let err = AppError::NotFound("User".into());
113        assert_eq!(err.to_string(), "Not Found: User");
114
115        let err = AppError::ValidationError("Invalid email".into());
116        assert_eq!(err.to_string(), "Validation Error: Invalid email");
117    }
118
119    #[test]
120    fn test_into_response() {
121        let err = AppError::Unauthorized("Invalid token".into());
122        let response = err.into_response();
123        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
124
125        let err = AppError::DatabaseError("Connection lost".into());
126        let response = err.into_response();
127        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
128    }
129}