elif_http/middleware/utils/
timeout.rs1use 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#[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!("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#[derive(Debug, Clone)]
163pub struct TimeoutInfo {
164 pub duration: Duration,
165 pub message: String,
166}
167
168pub 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#[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 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 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}