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