Skip to main content

nova_boot/
error.rs

1use axum::{
2    Json,
3    http::StatusCode,
4    response::{IntoResponse, Response},
5};
6use serde::{Deserialize, Serialize};
7use std::backtrace::Backtrace;
8use std::env;
9use std::fmt;
10
11/// Standardized JSON error response used by the framework.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ErrorResponse {
14    /// Short error type name (e.g., "NotFound", "DatabaseError").
15    pub error: String,
16
17    /// Human-readable message suitable for API clients.
18    pub message: String,
19
20    /// Optional details included only when `NOVA_DEBUG=true` to aid debugging.
21    pub details: Option<String>,
22}
23
24/// Nova framework error types.
25///
26/// Use `NovaError` for returning typed, HTTP-aware errors from handlers
27/// and library code. It implements `IntoResponse` so it can be returned
28/// directly from Axum handlers.
29#[derive(Debug)]
30pub enum NovaError {
31    /// Database connection or query errors
32    DatabaseError(String),
33
34    /// Validation failed
35    ValidationError(String),
36
37    /// Authentication failed
38    AuthenticationError(String),
39
40    /// Authorization failed (forbidden)
41    AuthorizationError(String),
42
43    /// Resource not found
44    NotFound(String),
45
46    /// Conflict error (e.g., duplicate entry)
47    Conflict(String),
48
49    /// Internal server error
50    InternalError(String),
51
52    /// Bad request / Invalid input
53    BadRequest(String),
54
55    /// Custom error with status code
56    Custom {
57        status: StatusCode,
58        error: String,
59        message: String,
60    },
61}
62
63impl fmt::Display for NovaError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            NovaError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
67            NovaError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
68            NovaError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
69            NovaError::AuthorizationError(msg) => write!(f, "Authorization error: {}", msg),
70            NovaError::NotFound(msg) => write!(f, "Not found: {}", msg),
71            NovaError::Conflict(msg) => write!(f, "Conflict: {}", msg),
72            NovaError::InternalError(msg) => write!(f, "Internal error: {}", msg),
73            NovaError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
74            NovaError::Custom { message, .. } => write!(f, "{}", message),
75        }
76    }
77}
78
79impl std::error::Error for NovaError {}
80
81/// Discovery subsystem errors.
82#[derive(Debug, Clone)]
83pub enum DiscoveryError {
84    NotFound(String),
85    Backend(String),
86    ConnectionFailed(String),
87}
88
89impl fmt::Display for DiscoveryError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::NotFound(msg) => write!(f, "Discovery not found: {}", msg),
93            Self::Backend(msg) => write!(f, "Discovery backend error: {}", msg),
94            Self::ConnectionFailed(msg) => write!(f, "Discovery connection failed: {}", msg),
95        }
96    }
97}
98
99impl std::error::Error for DiscoveryError {}
100
101impl NovaError {
102    /// Get the HTTP status code for this error
103    pub fn status_code(&self) -> StatusCode {
104        match self {
105            NovaError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
106            NovaError::ValidationError(_) => StatusCode::BAD_REQUEST,
107            NovaError::AuthenticationError(_) => StatusCode::UNAUTHORIZED,
108            NovaError::AuthorizationError(_) => StatusCode::FORBIDDEN,
109            NovaError::NotFound(_) => StatusCode::NOT_FOUND,
110            NovaError::Conflict(_) => StatusCode::CONFLICT,
111            NovaError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
112            NovaError::BadRequest(_) => StatusCode::BAD_REQUEST,
113            NovaError::Custom { status, .. } => *status,
114        }
115    }
116
117    /// Get the error type name
118    pub fn error_type(&self) -> &str {
119        match self {
120            NovaError::DatabaseError(_) => "DatabaseError",
121            NovaError::ValidationError(_) => "ValidationError",
122            NovaError::AuthenticationError(_) => "AuthenticationError",
123            NovaError::AuthorizationError(_) => "AuthorizationError",
124            NovaError::NotFound(_) => "NotFound",
125            NovaError::Conflict(_) => "Conflict",
126            NovaError::InternalError(_) => "InternalError",
127            NovaError::BadRequest(_) => "BadRequest",
128            NovaError::Custom { error, .. } => error,
129        }
130    }
131
132    /// Get the message
133    pub fn message(&self) -> String {
134        match self {
135            NovaError::Custom { message, .. } => message.clone(),
136            err => err.to_string(),
137        }
138    }
139}
140
141impl IntoResponse for NovaError {
142    fn into_response(self) -> Response {
143        let status = self.status_code();
144        // If `NOVA_DEBUG=true` is set, include a captured backtrace (which
145        // will contain file and line information when available) in the
146        // `details` field of the JSON error response. Otherwise omit details
147        // to avoid leaking internal information in production.
148        let details = match env::var("NOVA_DEBUG") {
149            Ok(val) if val.eq_ignore_ascii_case("true") || val == "1" => {
150                Some(format!("{:?}", Backtrace::capture()))
151            }
152            _ => None,
153        };
154
155        let body = Json(ErrorResponse {
156            error: self.error_type().to_string(),
157            message: self.message(),
158            details,
159        });
160
161        (status, body).into_response()
162    }
163}
164
165/// Convenience type alias for Results in Nova applications
166pub type NovaResult<T> = Result<T, NovaError>;
167
168// Common From implementations for easy error conversion
169impl From<std::io::Error> for NovaError {
170    fn from(err: std::io::Error) -> Self {
171        NovaError::InternalError(err.to_string())
172    }
173}
174
175impl From<serde_json::Error> for NovaError {
176    fn from(err: serde_json::Error) -> Self {
177        NovaError::BadRequest(format!("Invalid JSON: {}", err))
178    }
179}
180
181impl From<nova_boot_resilience_store::ResilienceError> for NovaError {
182    fn from(err: nova_boot_resilience_store::ResilienceError) -> Self {
183        NovaError::InternalError(err.to_string())
184    }
185}
186
187#[cfg(feature = "database")]
188impl From<sea_orm::DbErr> for NovaError {
189    fn from(err: sea_orm::DbErr) -> Self {
190        NovaError::DatabaseError(err.to_string())
191    }
192}