Skip to main content

nestforge_core/
error.rs

1use axum::{
2    http::StatusCode,
3    response::{IntoResponse, Response},
4    Json,
5};
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::ValidationErrors;
10
11/**
12* ErrorBody = standard JSON error response shape.
13*
14* Keeping this simple and clean for now:
15* {
16*   "statusCode": 500,
17*   "error": "Internal Server Error",
18*   "message": "Something went wrong"
19* }
20*/
21#[derive(Serialize)]
22struct ErrorBody {
23    #[serde(rename = "statusCode")]
24    status_code: u16,
25    error: String,
26    message: String,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    details: Option<Value>,
29}
30
31/**
32* HttpException = framework error type
33* This lets controllers return proper HTTP errors without manually building responses.
34*/
35#[derive(Debug, Clone)]
36pub struct HttpException {
37    pub status: StatusCode,
38    pub message: String,
39    pub details: Option<Value>,
40}
41
42impl HttpException {
43    /*
44    Generic constructor
45    */
46    pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
47        Self {
48            status,
49            message: message.into(),
50            details: None,
51        }
52    }
53
54    pub fn with_details(status: StatusCode, message: impl Into<String>, details: Value) -> Self {
55        Self {
56            status,
57            message: message.into(),
58            details: Some(details),
59        }
60    }
61
62    /*
63    Helper constructors (clean DX for controllers)
64    */
65    pub fn bad_request(message: impl Into<String>) -> Self {
66        Self::new(StatusCode::BAD_REQUEST, message)
67    }
68
69    pub fn bad_request_validation(errors: ValidationErrors) -> Self {
70        let message = "Validation failed".to_string();
71        let details = serde_json::to_value(errors).unwrap_or(Value::Null);
72        Self::with_details(StatusCode::BAD_REQUEST, message, details)
73    }
74
75    pub fn unauthorized(message: impl Into<String>) -> Self {
76        Self::new(StatusCode::UNAUTHORIZED, message)
77    }
78
79    pub fn not_found(message: impl Into<String>) -> Self {
80        Self::new(StatusCode::NOT_FOUND, message)
81    }
82
83    pub fn internal_server_error(message: impl Into<String>) -> Self {
84        Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
85    }
86}
87
88/**
89* IntoResponse makes HttpException directly returnable from axum handlers.
90*
91* So handlers can return:
92* Result<Json<T>, HttpException>
93* and axum knows how to turn the error into a real HTTP response.
94*/
95impl IntoResponse for HttpException {
96    fn into_response(self) -> Response {
97        let error_name = self
98            .status
99            .canonical_reason()
100            .unwrap_or("Error")
101            .to_string();
102
103        let body = ErrorBody {
104            status_code: self.status.as_u16(),
105            error: error_name,
106            message: self.message,
107            details: self.details,
108        };
109
110        (self.status, Json(body)).into_response()
111    }
112}