elif_http/middleware/core/
error_handler.rs

1//! Error handling middleware for HTTP requests
2//!
3//! Provides comprehensive error handling including panic recovery and error response formatting.
4
5use crate::{
6    errors::HttpError,
7    middleware::v2::{Middleware, Next, NextFuture},
8    request::ElifRequest,
9    response::IntoElifResponse,
10};
11use futures_util::future::FutureExt;
12
13/// Error handling middleware configuration
14#[derive(Debug, Clone)]
15pub struct ErrorHandlerConfig {
16    /// Whether to include panic details in error responses (development only)
17    pub include_panic_details: bool,
18
19    /// Whether to log errors
20    pub log_errors: bool,
21}
22
23impl Default for ErrorHandlerConfig {
24    fn default() -> Self {
25        Self {
26            include_panic_details: cfg!(debug_assertions), // Only in debug builds
27            log_errors: true,
28        }
29    }
30}
31
32/// Error handling middleware
33#[derive(Debug)]
34pub struct ErrorHandlerMiddleware {
35    config: ErrorHandlerConfig,
36}
37
38impl ErrorHandlerMiddleware {
39    /// Create new error handling middleware with default config
40    pub fn new() -> Self {
41        Self {
42            config: ErrorHandlerConfig::default(),
43        }
44    }
45
46    /// Create with custom configuration
47    pub fn with_config(config: ErrorHandlerConfig) -> Self {
48        Self { config }
49    }
50
51    /// Enable panic details in responses (use only in development)
52    pub fn with_panic_details(mut self, include: bool) -> Self {
53        self.config.include_panic_details = include;
54        self
55    }
56
57    /// Enable error logging
58    pub fn with_logging(mut self, enable: bool) -> Self {
59        self.config.log_errors = enable;
60        self
61    }
62}
63
64impl Default for ErrorHandlerMiddleware {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl Middleware for ErrorHandlerMiddleware {
71    fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
72        let config = self.config.clone();
73        Box::pin(async move {
74            // The future from `next.run()` might panic, so we catch it.
75            // `AssertUnwindSafe` is used because the handler might not be `UnwindSafe`.
76            let result = std::panic::AssertUnwindSafe(next.run(request))
77                .catch_unwind()
78                .await;
79
80            match result {
81                Ok(response) => response,
82                Err(panic_info) => {
83                    // A panic occurred, so we create an error response.
84                    let panic_message = if let Some(s) = panic_info.downcast_ref::<String>() {
85                        s.clone()
86                    } else if let Some(s) = panic_info.downcast_ref::<&str>() {
87                        s.to_string()
88                    } else {
89                        "Unknown panic occurred".to_string()
90                    };
91
92                    if config.log_errors {
93                        // Using tracing::error for logging panics.
94                        tracing::error!("Panic in request handler: {}", panic_message);
95                    }
96
97                    let error_message = if config.include_panic_details {
98                        format!("Internal server error: {}", panic_message)
99                    } else {
100                        "Internal server error occurred".to_string()
101                    };
102
103                    let http_error = HttpError::internal(error_message);
104                    http_error.into_response()
105                }
106            }
107        })
108    }
109
110    fn name(&self) -> &'static str {
111        "ErrorHandlerMiddleware"
112    }
113}
114
115/// Helper function to create error handler middleware
116pub fn error_handler() -> ErrorHandlerMiddleware {
117    ErrorHandlerMiddleware::new()
118}
119
120/// Helper function to create error handler middleware with config
121pub fn error_handler_with_config(config: ErrorHandlerConfig) -> ErrorHandlerMiddleware {
122    ErrorHandlerMiddleware::with_config(config)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::{
129        middleware::v2::MiddlewarePipelineV2,
130        request::ElifMethod,
131        response::{headers::ElifHeaderMap, ElifResponse},
132    };
133
134    #[tokio::test]
135    async fn test_error_handler_config() {
136        let config = ErrorHandlerConfig {
137            include_panic_details: true,
138            log_errors: false,
139        };
140
141        assert!(config.include_panic_details);
142        assert!(!config.log_errors);
143    }
144
145    #[tokio::test]
146    async fn test_error_handler_middleware_creation() {
147        let middleware = ErrorHandlerMiddleware::new()
148            .with_panic_details(true)
149            .with_logging(false);
150
151        assert!(middleware.config.include_panic_details);
152        assert!(!middleware.config.log_errors);
153    }
154
155    #[tokio::test]
156    async fn test_error_handler_with_http_error() {
157        let middleware = ErrorHandlerMiddleware::new();
158
159        let request = ElifRequest::new(
160            ElifMethod::GET,
161            "/test".parse().unwrap(),
162            ElifHeaderMap::new(),
163        );
164
165        let next = crate::middleware::v2::Next::new(|_req| {
166            Box::pin(async {
167                // Return an error response
168                HttpError::bad_request("Test error").into_response()
169            })
170        });
171
172        let response = middleware.handle(request, next).await;
173        assert_eq!(
174            response.status_code(),
175            crate::response::status::ElifStatusCode::BAD_REQUEST
176        );
177    }
178
179    #[tokio::test]
180    async fn test_error_handler_normal_flow() {
181        let middleware = ErrorHandlerMiddleware::new();
182        let pipeline = MiddlewarePipelineV2::new().add(middleware);
183
184        let request = ElifRequest::new(
185            ElifMethod::GET,
186            "/test".parse().unwrap(),
187            ElifHeaderMap::new(),
188        );
189
190        let response = pipeline
191            .execute(request, |_req| {
192                Box::pin(async {
193                    ElifResponse::ok().json_value(serde_json::json!({
194                        "message": "Success"
195                    }))
196                })
197            })
198            .await;
199
200        assert_eq!(
201            response.status_code(),
202            crate::response::status::ElifStatusCode::OK
203        );
204    }
205
206    #[test]
207    fn test_error_handler_config_default() {
208        let config = ErrorHandlerConfig::default();
209
210        // Should include panic details only in debug mode
211        assert_eq!(config.include_panic_details, cfg!(debug_assertions));
212        assert!(config.log_errors);
213    }
214}