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 crate::{
7    middleware::v2::{Middleware, Next, NextFuture},
8    request::ElifRequest,
9    response::{ElifResponse, ElifStatusCode},
10};
11use serde_json;
12use std::time::Duration;
13use tokio::time::timeout;
14use tracing::{error, warn};
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!(
141                            "Request timed out after {:?}: {}",
142                            timeout_duration, timeout_message
143                        );
144                    }
145
146                    ElifResponse::with_status(ElifStatusCode::REQUEST_TIMEOUT).json_value(
147                        serde_json::json!({
148                            "error": {
149                                "code": "REQUEST_TIMEOUT",
150                                "message": &timeout_message,
151                                "timeout_duration_secs": timeout_duration.as_secs()
152                            }
153                        }),
154                    )
155                }
156            }
157        })
158    }
159
160    fn name(&self) -> &'static str {
161        "TimeoutMiddleware"
162    }
163}
164
165/// Timeout information stored in request extensions
166#[derive(Debug, Clone)]
167pub struct TimeoutInfo {
168    pub duration: Duration,
169    pub message: String,
170}
171
172/// Helper function to apply timeout to a future
173pub async fn apply_timeout<F, T>(
174    future: F,
175    duration: Duration,
176    timeout_message: &str,
177) -> Result<T, ElifResponse>
178where
179    F: std::future::Future<Output = T>,
180{
181    match timeout(duration, future).await {
182        Ok(result) => Ok(result),
183        Err(_) => {
184            error!(
185                "Request timed out after {:?}: {}",
186                duration, timeout_message
187            );
188            Err(
189                ElifResponse::with_status(ElifStatusCode::REQUEST_TIMEOUT).json_value(
190                    serde_json::json!({
191                        "error": {
192                            "code": "REQUEST_TIMEOUT",
193                            "message": timeout_message,
194                            "timeout_duration_secs": duration.as_secs()
195                        }
196                    }),
197                ),
198            )
199        }
200    }
201}
202
203// TimeoutHandler removed - use TimeoutMiddleware with V2 system instead
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{middleware::v2::Next, request::ElifRequest};
209    use std::time::Duration;
210    use tokio::time::{sleep, Duration as TokioDuration};
211
212    #[tokio::test]
213    async fn test_timeout_middleware_fast_response() {
214        let middleware = TimeoutMiddleware::with_duration(Duration::from_secs(1));
215
216        let request = ElifRequest::new(
217            crate::request::ElifMethod::GET,
218            "/test".parse().unwrap(),
219            crate::response::headers::ElifHeaderMap::new(),
220        );
221
222        let next = Next::new(|_req| Box::pin(async { ElifResponse::ok().text("Fast response") }));
223
224        let response = middleware.handle(request, next).await;
225        assert_eq!(response.status_code(), crate::response::ElifStatusCode::OK);
226    }
227
228    #[tokio::test]
229    async fn test_timeout_middleware_slow_response() {
230        let middleware = TimeoutMiddleware::with_duration(Duration::from_millis(100));
231
232        let request = ElifRequest::new(
233            crate::request::ElifMethod::GET,
234            "/test".parse().unwrap(),
235            crate::response::headers::ElifHeaderMap::new(),
236        );
237
238        let next = Next::new(|_req| {
239            Box::pin(async {
240                // Slow response that will timeout
241                sleep(TokioDuration::from_millis(200)).await;
242                ElifResponse::ok().text("Should not reach here")
243            })
244        });
245
246        let response = middleware.handle(request, next).await;
247        assert_eq!(
248            response.status_code(),
249            crate::response::ElifStatusCode::REQUEST_TIMEOUT
250        );
251    }
252
253    #[tokio::test]
254    async fn test_timeout_middleware_custom_config() {
255        let config = TimeoutConfig::new(Duration::from_secs(60))
256            .with_logging(false)
257            .with_message("Custom timeout");
258
259        let middleware = TimeoutMiddleware::with_config(config);
260
261        assert_eq!(middleware.duration(), Duration::from_secs(60));
262        assert!(!middleware.config.log_timeouts);
263        assert_eq!(middleware.config.timeout_message, "Custom timeout");
264    }
265
266    #[tokio::test]
267    async fn test_timeout_middleware_builder() {
268        let middleware = TimeoutMiddleware::new()
269            .timeout(Duration::from_secs(45))
270            .logging(true)
271            .message("Builder timeout");
272
273        assert_eq!(middleware.duration(), Duration::from_secs(45));
274        assert!(middleware.config.log_timeouts);
275        assert_eq!(middleware.config.timeout_message, "Builder timeout");
276    }
277
278    #[tokio::test]
279    async fn test_timeout_middleware_name() {
280        let middleware = TimeoutMiddleware::new();
281        assert_eq!(middleware.name(), "TimeoutMiddleware");
282    }
283
284    #[tokio::test]
285    async fn test_apply_timeout_success() {
286        let future = async { "success" };
287        let result = apply_timeout(future, Duration::from_secs(1), "test timeout").await;
288
289        assert!(result.is_ok());
290        assert_eq!(result.unwrap(), "success");
291    }
292
293    #[tokio::test]
294    async fn test_apply_timeout_failure() {
295        let future = async {
296            sleep(TokioDuration::from_secs(2)).await;
297            "should not reach here"
298        };
299
300        let result = apply_timeout(future, Duration::from_millis(100), "test timeout").await;
301        assert!(result.is_err());
302
303        // Verify it's a timeout response
304        let response = result.unwrap_err();
305        assert_eq!(
306            response.status_code(),
307            crate::response::ElifStatusCode::REQUEST_TIMEOUT
308        );
309    }
310
311    #[tokio::test]
312    async fn test_timeout_config_defaults() {
313        let config = TimeoutConfig::default();
314
315        assert_eq!(config.timeout, Duration::from_secs(30));
316        assert!(config.log_timeouts);
317        assert_eq!(config.timeout_message, "Request timed out");
318    }
319}