elif_http/
error.rs

1//! HTTP server error types
2//! 
3//! Comprehensive error handling for HTTP operations, integrating with
4//! the elif framework error system.
5
6use thiserror::Error;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10use serde_json::json;
11use crate::response::{ElifResponse, IntoElifResponse};
12
13/// Result type for HTTP operations
14pub type HttpResult<T> = Result<T, HttpError>;
15
16/// HTTP server errors
17#[derive(Error, Debug)]
18pub enum HttpError {
19    #[error("Server startup failed: {message}")]
20    StartupFailed { message: String },
21    
22    #[error("Server shutdown failed: {message}")]
23    ShutdownFailed { message: String },
24    
25    #[error("Configuration error: {message}")]
26    ConfigError { message: String },
27    
28    #[error("Service resolution failed: {service}")]
29    ServiceResolutionFailed { service: String },
30    
31    #[error("Request timeout")]
32    RequestTimeout,
33    
34    #[error("Request too large: {size} bytes exceeds limit of {limit} bytes")]
35    RequestTooLarge { size: usize, limit: usize },
36    
37    #[error("Invalid request: {message}")]
38    BadRequest { message: String },
39    
40    #[error("Internal server error: {message}")]
41    InternalError { message: String },
42    
43    #[error("Health check failed: {reason}")]
44    HealthCheckFailed { reason: String },
45    
46    #[error("Database error: {message}")]
47    DatabaseError { message: String },
48    
49    #[error("Validation error: {message}")]
50    ValidationError { message: String },
51    
52    #[error("Resource not found: {resource}")]
53    NotFound { resource: String },
54    
55    #[error("Resource already exists: {message}")]
56    Conflict { message: String },
57    
58    #[error("Unauthorized access")]
59    Unauthorized,
60    
61    #[error("Access forbidden: {message}")]
62    Forbidden { message: String },
63}
64
65impl HttpError {
66    /// Create a startup error
67    pub fn startup<T: Into<String>>(message: T) -> Self {
68        HttpError::StartupFailed { 
69            message: message.into() 
70        }
71    }
72    
73    /// Create a shutdown error
74    pub fn shutdown<T: Into<String>>(message: T) -> Self {
75        HttpError::ShutdownFailed { 
76            message: message.into() 
77        }
78    }
79    
80    /// Create a configuration error
81    pub fn config<T: Into<String>>(message: T) -> Self {
82        HttpError::ConfigError { 
83            message: message.into() 
84        }
85    }
86    
87    /// Create a service resolution error
88    pub fn service_resolution<T: Into<String>>(service: T) -> Self {
89        HttpError::ServiceResolutionFailed { 
90            service: service.into() 
91        }
92    }
93    
94    /// Create a bad request error
95    pub fn bad_request<T: Into<String>>(message: T) -> Self {
96        HttpError::BadRequest { 
97            message: message.into() 
98        }
99    }
100    
101    /// Create an internal error
102    pub fn internal<T: Into<String>>(message: T) -> Self {
103        HttpError::InternalError { 
104            message: message.into() 
105        }
106    }
107    
108    /// Create a health check error
109    pub fn health_check<T: Into<String>>(reason: T) -> Self {
110        HttpError::HealthCheckFailed { 
111            reason: reason.into() 
112        }
113    }
114    
115    /// Create a database error
116    pub fn database_error<T: Into<String>>(message: T) -> Self {
117        HttpError::DatabaseError { 
118            message: message.into() 
119        }
120    }
121    
122    /// Create a validation error
123    pub fn validation_error<T: Into<String>>(message: T) -> Self {
124        HttpError::ValidationError { 
125            message: message.into() 
126        }
127    }
128    
129    /// Create a not found error
130    pub fn not_found<T: Into<String>>(resource: T) -> Self {
131        HttpError::NotFound { 
132            resource: resource.into() 
133        }
134    }
135    
136    /// Create a conflict error
137    pub fn conflict<T: Into<String>>(message: T) -> Self {
138        HttpError::Conflict { 
139            message: message.into() 
140        }
141    }
142    
143    /// Create an unauthorized error
144    pub fn unauthorized() -> Self {
145        HttpError::Unauthorized
146    }
147    
148    /// Create a forbidden error
149    pub fn forbidden<T: Into<String>>(message: T) -> Self {
150        HttpError::Forbidden { 
151            message: message.into() 
152        }
153    }
154    
155    /// Create an internal server error
156    pub fn internal_server_error<T: Into<String>>(message: T) -> Self {
157        HttpError::InternalError { 
158            message: message.into() 
159        }
160    }
161
162    /// Create a timeout error
163    pub fn timeout<T: Into<String>>(_message: T) -> Self {
164        HttpError::RequestTimeout
165    }
166
167    /// Create a payload too large error
168    pub fn payload_too_large<T: Into<String>>(_message: T) -> Self {
169        HttpError::RequestTooLarge { 
170            size: 0, // Will be set dynamically if needed
171            limit: 0
172        }
173    }
174
175    /// Create a payload too large error with specific sizes
176    pub fn payload_too_large_with_sizes<T: Into<String>>(_message: T, size: usize, limit: usize) -> Self {
177        HttpError::RequestTooLarge { size, limit }
178    }
179
180    /// Add additional detail to error (for now, just returns self - future enhancement)
181    pub fn with_detail<T: Into<String>>(self, _detail: T) -> Self {
182        self
183    }
184
185    /// Get the appropriate HTTP status code for this error
186    pub fn status_code(&self) -> StatusCode {
187        match self {
188            HttpError::StartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
189            HttpError::ShutdownFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
190            HttpError::ConfigError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
191            HttpError::ServiceResolutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
192            HttpError::RequestTimeout => StatusCode::REQUEST_TIMEOUT,
193            HttpError::RequestTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
194            HttpError::BadRequest { .. } => StatusCode::BAD_REQUEST,
195            HttpError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
196            HttpError::HealthCheckFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
197            HttpError::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
198            HttpError::ValidationError { .. } => StatusCode::BAD_REQUEST,
199            HttpError::NotFound { .. } => StatusCode::NOT_FOUND,
200            HttpError::Conflict { .. } => StatusCode::CONFLICT,
201            HttpError::Unauthorized => StatusCode::UNAUTHORIZED,
202            HttpError::Forbidden { .. } => StatusCode::FORBIDDEN,
203        }
204    }
205
206    /// Get error code for consistent API responses
207    pub fn error_code(&self) -> &'static str {
208        match self {
209            HttpError::StartupFailed { .. } => "SERVER_STARTUP_FAILED",
210            HttpError::ShutdownFailed { .. } => "SERVER_SHUTDOWN_FAILED",
211            HttpError::ConfigError { .. } => "CONFIGURATION_ERROR",
212            HttpError::ServiceResolutionFailed { .. } => "SERVICE_RESOLUTION_FAILED",
213            HttpError::RequestTimeout => "REQUEST_TIMEOUT",
214            HttpError::RequestTooLarge { .. } => "REQUEST_TOO_LARGE",
215            HttpError::BadRequest { .. } => "BAD_REQUEST",
216            HttpError::InternalError { .. } => "INTERNAL_ERROR",
217            HttpError::HealthCheckFailed { .. } => "HEALTH_CHECK_FAILED",
218            HttpError::DatabaseError { .. } => "DATABASE_ERROR",
219            HttpError::ValidationError { .. } => "VALIDATION_ERROR",
220            HttpError::NotFound { .. } => "RESOURCE_NOT_FOUND",
221            HttpError::Conflict { .. } => "RESOURCE_CONFLICT",
222            HttpError::Unauthorized => "UNAUTHORIZED_ACCESS",
223            HttpError::Forbidden { .. } => "ACCESS_FORBIDDEN",
224        }
225    }
226}
227
228// Implement IntoElifResponse for HttpError
229impl IntoElifResponse for HttpError {
230    fn into_elif_response(self) -> ElifResponse {
231        let body = json!({
232            "error": {
233                "code": self.error_code(),
234                "message": self.to_string(),
235                "hint": match &self {
236                    HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
237                    HttpError::RequestTimeout => Some("Retry the request"),
238                    HttpError::BadRequest { .. } => Some("Check request format and parameters"),
239                    HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
240                    _ => None,
241                }
242            }
243        });
244
245        ElifResponse::with_status(self.status_code())
246            .json_value(body)
247    }
248}
249
250// Implement IntoResponse for automatic HTTP error responses
251impl IntoResponse for HttpError {
252    fn into_response(self) -> Response {
253        let status = self.status_code();
254        let body = json!({
255            "error": {
256                "code": self.error_code(),
257                "message": self.to_string(),
258                "hint": match &self {
259                    HttpError::RequestTooLarge { .. } => Some("Reduce request payload size"),
260                    HttpError::RequestTimeout => Some("Retry the request"),
261                    HttpError::BadRequest { .. } => Some("Check request format and parameters"),
262                    HttpError::HealthCheckFailed { .. } => Some("Server may be starting up or experiencing issues"),
263                    _ => None,
264                }
265            }
266        });
267
268        (status, Json(body)).into_response()
269    }
270}
271
272// Convert from elif-core ConfigError
273impl From<elif_core::app_config::ConfigError> for HttpError {
274    fn from(err: elif_core::app_config::ConfigError) -> Self {
275        HttpError::ConfigError { 
276            message: err.to_string() 
277        }
278    }
279}
280
281// Convert from std::io::Error
282impl From<std::io::Error> for HttpError {
283    fn from(err: std::io::Error) -> Self {
284        HttpError::InternalError { 
285            message: format!("IO error: {}", err) 
286        }
287    }
288}
289
290// Convert from hyper errors
291impl From<hyper::Error> for HttpError {
292    fn from(err: hyper::Error) -> Self {
293        HttpError::InternalError { 
294            message: format!("Hyper error: {}", err) 
295        }
296    }
297}
298
299// Convert from ORM ModelError
300impl From<elif_orm::ModelError> for HttpError {
301    fn from(err: elif_orm::ModelError) -> Self {
302        match err {
303            elif_orm::ModelError::Database(msg) => HttpError::DatabaseError { message: msg },
304            elif_orm::ModelError::Validation(msg) => HttpError::ValidationError { message: msg },
305            elif_orm::ModelError::NotFound(resource) => HttpError::NotFound { resource },
306            elif_orm::ModelError::Serialization(msg) => HttpError::InternalError { 
307                message: format!("Serialization error: {}", msg) 
308            },
309            _ => HttpError::InternalError { 
310                message: format!("ORM error: {}", err) 
311            },
312        }
313    }
314}
315
316// Convert from serde_json errors
317impl From<serde_json::Error> for HttpError {
318    fn from(err: serde_json::Error) -> Self {
319        HttpError::InternalError { 
320            message: format!("JSON serialization error: {}", err) 
321        }
322    }
323}
324
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_error_creation() {
332        let error = HttpError::startup("Failed to bind to port");
333        assert!(matches!(error, HttpError::StartupFailed { .. }));
334        assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
335        assert_eq!(error.error_code(), "SERVER_STARTUP_FAILED");
336    }
337
338    #[test]
339    fn test_error_status_codes() {
340        assert_eq!(HttpError::bad_request("test").status_code(), StatusCode::BAD_REQUEST);
341        assert_eq!(HttpError::RequestTimeout.status_code(), StatusCode::REQUEST_TIMEOUT);
342        assert_eq!(
343            HttpError::RequestTooLarge { size: 100, limit: 50 }.status_code(), 
344            StatusCode::PAYLOAD_TOO_LARGE
345        );
346        assert_eq!(
347            HttpError::health_check("Database unavailable").status_code(), 
348            StatusCode::SERVICE_UNAVAILABLE
349        );
350    }
351
352    #[test]
353    fn test_error_codes() {
354        assert_eq!(HttpError::bad_request("test").error_code(), "BAD_REQUEST");
355        assert_eq!(HttpError::RequestTimeout.error_code(), "REQUEST_TIMEOUT");
356        assert_eq!(HttpError::internal("test").error_code(), "INTERNAL_ERROR");
357    }
358
359    #[test]
360    fn test_config_error_conversion() {
361        let config_error = elif_core::app_config::ConfigError::MissingEnvVar {
362            var: "TEST_VAR".to_string(),
363        };
364        let http_error = HttpError::from(config_error);
365        assert!(matches!(http_error, HttpError::ConfigError { .. }));
366    }
367
368    #[test]
369    fn test_io_error_conversion() {
370        let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
371        let http_error = HttpError::from(io_error);
372        assert!(matches!(http_error, HttpError::InternalError { .. }));
373    }
374}