elif_http/middleware/utils/
timeout.rs1use 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#[derive(Debug, Clone)]
18pub struct TimeoutConfig {
19 pub timeout: Duration,
21 pub log_timeouts: bool,
23 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 pub fn new(timeout: Duration) -> Self {
40 Self {
41 timeout,
42 ..Default::default()
43 }
44 }
45
46 pub fn with_timeout(mut self, timeout: Duration) -> Self {
48 self.timeout = timeout;
49 self
50 }
51
52 pub fn with_logging(mut self, log_timeouts: bool) -> Self {
54 self.log_timeouts = log_timeouts;
55 self
56 }
57
58 pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
60 self.timeout_message = message.into();
61 self
62 }
63}
64
65#[derive(Debug)]
67pub struct TimeoutMiddleware {
68 config: TimeoutConfig,
69}
70
71impl TimeoutMiddleware {
72 pub fn new() -> Self {
74 Self {
75 config: TimeoutConfig::default(),
76 }
77 }
78
79 pub fn with_duration(timeout: Duration) -> Self {
81 Self {
82 config: TimeoutConfig::new(timeout),
83 }
84 }
85
86 pub fn with_config(config: TimeoutConfig) -> Self {
88 Self { config }
89 }
90
91 pub fn timeout(mut self, duration: Duration) -> Self {
93 self.config = self.config.with_timeout(duration);
94 self
95 }
96
97 pub fn logging(mut self, enabled: bool) -> Self {
99 self.config = self.config.with_logging(enabled);
100 self
101 }
102
103 pub fn message<S: Into<String>>(mut self, message: S) -> Self {
105 self.config = self.config.with_message(message);
106 self
107 }
108
109 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 match timeout(timeout_duration, next.run(request)).await {
130 Ok(response) => {
131 if response.status_code() == ElifStatusCode::REQUEST_TIMEOUT && log_timeouts {
133 warn!("Request timed out after {:?}", timeout_duration);
134 }
135 response
136 }
137 Err(_) => {
138 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#[derive(Debug, Clone)]
167pub struct TimeoutInfo {
168 pub duration: Duration,
169 pub message: String,
170}
171
172pub 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#[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 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 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}