elif_http/middleware/utils/
timeout.rs

1//! # Timeout Middleware
2//!
3//! Framework middleware for request timeout handling.
4//! Replaces tower-http TimeoutLayer with framework-native implementation.
5
6use std::time::Duration;
7use tokio::time::timeout;
8use tracing::{warn, error};
9use serde_json;
10use crate::{
11    middleware::v2::{Middleware, Next, NextFuture},
12    request::ElifRequest,
13    response::{ElifResponse, ElifStatusCode},
14};
15
16/// Configuration for timeout middleware
17#[derive(Debug, Clone)]
18pub struct TimeoutConfig {
19    /// Request timeout duration
20    pub timeout: Duration,
21    /// Whether to log timeout events
22    pub log_timeouts: bool,
23    /// Custom timeout error message
24    pub timeout_message: String,
25}
26
27impl Default for TimeoutConfig {
28    fn default() -> Self {
29        Self {
30            timeout: Duration::from_secs(30),
31            log_timeouts: true,
32            timeout_message: "Request timed out".to_string(),
33        }
34    }
35}
36
37impl TimeoutConfig {
38    /// Create new timeout configuration
39    pub fn new(timeout: Duration) -> Self {
40        Self {
41            timeout,
42            ..Default::default()
43        }
44    }
45
46    /// Set timeout duration
47    pub fn with_timeout(mut self, timeout: Duration) -> Self {
48        self.timeout = timeout;
49        self
50    }
51
52    /// Enable or disable timeout logging
53    pub fn with_logging(mut self, log_timeouts: bool) -> Self {
54        self.log_timeouts = log_timeouts;
55        self
56    }
57
58    /// Set custom timeout error message
59    pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
60        self.timeout_message = message.into();
61        self
62    }
63}
64
65/// Framework timeout middleware for HTTP requests
66#[derive(Debug)]
67pub struct TimeoutMiddleware {
68    config: TimeoutConfig,
69}
70
71impl TimeoutMiddleware {
72    /// Create new timeout middleware with default 30 second timeout
73    pub fn new() -> Self {
74        Self {
75            config: TimeoutConfig::default(),
76        }
77    }
78
79    /// Create timeout middleware with specific duration
80    pub fn with_duration(timeout: Duration) -> Self {
81        Self {
82            config: TimeoutConfig::new(timeout),
83        }
84    }
85
86    /// Create timeout middleware with custom configuration
87    pub fn with_config(config: TimeoutConfig) -> Self {
88        Self { config }
89    }
90
91    /// Set timeout duration (builder pattern)
92    pub fn timeout(mut self, duration: Duration) -> Self {
93        self.config = self.config.with_timeout(duration);
94        self
95    }
96
97    /// Enable or disable logging (builder pattern) 
98    pub fn logging(mut self, enabled: bool) -> Self {
99        self.config = self.config.with_logging(enabled);
100        self
101    }
102
103    /// Set custom timeout message (builder pattern)
104    pub fn message<S: Into<String>>(mut self, message: S) -> Self {
105        self.config = self.config.with_message(message);
106        self
107    }
108
109    /// Get timeout duration
110    pub fn duration(&self) -> Duration {
111        self.config.timeout
112    }
113}
114
115impl Default for TimeoutMiddleware {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl Middleware for TimeoutMiddleware {
122    fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
123        let timeout_duration = self.config.timeout;
124        let log_timeouts = self.config.log_timeouts;
125        let timeout_message = self.config.timeout_message.clone();
126        
127        Box::pin(async move {
128            // Apply timeout to the entire middleware chain
129            match timeout(timeout_duration, next.run(request)).await {
130                Ok(response) => {
131                    // Check if response indicates timeout and log if enabled
132                    if response.status_code() == ElifStatusCode::REQUEST_TIMEOUT && log_timeouts {
133                        warn!("Request timed out after {:?}", timeout_duration);
134                    }
135                    response
136                }
137                Err(_) => {
138                    // Timeout occurred
139                    if log_timeouts {
140                        error!("Request timed out after {:?}: {}", timeout_duration, timeout_message);
141                    }
142                    
143                    ElifResponse::with_status(ElifStatusCode::REQUEST_TIMEOUT)
144                        .json_value(serde_json::json!({
145                            "error": {
146                                "code": "REQUEST_TIMEOUT",
147                                "message": &timeout_message,
148                                "timeout_duration_secs": timeout_duration.as_secs()
149                            }
150                        }))
151                }
152            }
153        })
154    }
155
156    fn name(&self) -> &'static str {
157        "TimeoutMiddleware"
158    }
159}
160
161/// Timeout information stored in request extensions
162#[derive(Debug, Clone)]
163pub struct TimeoutInfo {
164    pub duration: Duration,
165    pub message: String,
166}
167
168/// Helper function to apply timeout to a future
169pub async fn apply_timeout<F, T>(
170    future: F,
171    duration: Duration,
172    timeout_message: &str,
173) -> Result<T, ElifResponse>
174where
175    F: std::future::Future<Output = T>,
176{
177    match timeout(duration, future).await {
178        Ok(result) => Ok(result),
179        Err(_) => {
180            error!("Request timed out after {:?}: {}", duration, timeout_message);
181            Err(ElifResponse::with_status(ElifStatusCode::REQUEST_TIMEOUT)
182                .json_value(serde_json::json!({
183                    "error": {
184                        "code": "REQUEST_TIMEOUT",
185                        "message": timeout_message,
186                        "timeout_duration_secs": duration.as_secs()
187                    }
188                })))
189        }
190    }
191}
192
193// TimeoutHandler removed - use TimeoutMiddleware with V2 system instead
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::{middleware::v2::Next, request::ElifRequest};
199    use tokio::time::{sleep, Duration as TokioDuration};
200    use std::time::Duration;
201
202    #[tokio::test]
203    async fn test_timeout_middleware_fast_response() {
204        let middleware = TimeoutMiddleware::with_duration(Duration::from_secs(1));
205        
206        let request = ElifRequest::new(
207            crate::request::ElifMethod::GET,
208            "/test".parse().unwrap(),
209            crate::response::headers::ElifHeaderMap::new(),
210        );
211
212        let next = Next::new(|_req| {
213            Box::pin(async {
214                ElifResponse::ok().text("Fast response")
215            })
216        });
217
218        let response = middleware.handle(request, next).await;
219        assert_eq!(response.status_code(), crate::response::ElifStatusCode::OK);
220    }
221
222    #[tokio::test]
223    async fn test_timeout_middleware_slow_response() {
224        let middleware = TimeoutMiddleware::with_duration(Duration::from_millis(100));
225        
226        let request = ElifRequest::new(
227            crate::request::ElifMethod::GET,
228            "/test".parse().unwrap(),
229            crate::response::headers::ElifHeaderMap::new(),
230        );
231
232        let next = Next::new(|_req| {
233            Box::pin(async {
234                // Slow response that will timeout
235                sleep(TokioDuration::from_millis(200)).await;
236                ElifResponse::ok().text("Should not reach here")
237            })
238        });
239
240        let response = middleware.handle(request, next).await;
241        assert_eq!(response.status_code(), crate::response::ElifStatusCode::REQUEST_TIMEOUT);
242    }
243
244    #[tokio::test]
245    async fn test_timeout_middleware_custom_config() {
246        let config = TimeoutConfig::new(Duration::from_secs(60))
247            .with_logging(false)
248            .with_message("Custom timeout");
249
250        let middleware = TimeoutMiddleware::with_config(config);
251        
252        assert_eq!(middleware.duration(), Duration::from_secs(60));
253        assert!(!middleware.config.log_timeouts);
254        assert_eq!(middleware.config.timeout_message, "Custom timeout");
255    }
256
257    #[tokio::test]
258    async fn test_timeout_middleware_builder() {
259        let middleware = TimeoutMiddleware::new()
260            .timeout(Duration::from_secs(45))
261            .logging(true)
262            .message("Builder timeout");
263        
264        assert_eq!(middleware.duration(), Duration::from_secs(45));
265        assert!(middleware.config.log_timeouts);
266        assert_eq!(middleware.config.timeout_message, "Builder timeout");
267    }
268
269    #[tokio::test]
270    async fn test_timeout_middleware_name() {
271        let middleware = TimeoutMiddleware::new();
272        assert_eq!(middleware.name(), "TimeoutMiddleware");
273    }
274
275    #[tokio::test]
276    async fn test_apply_timeout_success() {
277        let future = async { "success" };
278        let result = apply_timeout(future, Duration::from_secs(1), "test timeout").await;
279        
280        assert!(result.is_ok());
281        assert_eq!(result.unwrap(), "success");
282    }
283
284    #[tokio::test]
285    async fn test_apply_timeout_failure() {
286        let future = async {
287            sleep(TokioDuration::from_secs(2)).await;
288            "should not reach here"
289        };
290        
291        let result = apply_timeout(future, Duration::from_millis(100), "test timeout").await;
292        assert!(result.is_err());
293        
294        // Verify it's a timeout response
295        let response = result.unwrap_err();
296        assert_eq!(response.status_code(), crate::response::ElifStatusCode::REQUEST_TIMEOUT);
297    }
298
299    #[tokio::test]
300    async fn test_timeout_config_defaults() {
301        let config = TimeoutConfig::default();
302        
303        assert_eq!(config.timeout, Duration::from_secs(30));
304        assert!(config.log_timeouts);
305        assert_eq!(config.timeout_message, "Request timed out");
306    }
307}