rustlavel_http/
timeout.rs1use crate::handler::BoxFuture;
28use crate::middleware::{Middleware, Next};
29use crate::request::Request;
30use crate::response::Response;
31use crate::status::Status;
32use rustlavel_core::Json;
33use std::time::Duration;
34
35#[derive(Debug, Clone, Copy)]
36pub struct Timeout {
37 limit: Duration,
38}
39
40impl Timeout {
41 pub fn after(limit: Duration) -> Self {
42 Timeout { limit }
43 }
44
45 pub fn seconds(seconds: u64) -> Self {
46 Timeout::after(Duration::from_secs(seconds))
47 }
48}
49
50impl Middleware for Timeout {
51 fn handle(&self, request: Request, next: Next) -> BoxFuture<Response> {
52 let limit = self.limit;
53 let (method, path) = (request.method(), request.path().to_string());
54 let wants_json = request.wants_json();
55
56 Box::pin(async move {
57 match tokio::time::timeout(limit, next.run(request)).await {
58 Ok(response) => response,
59 Err(_) => {
60 rustlavel_core::warn!(
61 "{method} {path} did not finish within {} ms and was abandoned",
62 limit.as_millis()
63 );
64 let response = Response::new(Status::SERVICE_UNAVAILABLE);
65 if wants_json {
66 response.with_json(Json::object([(
67 "message",
68 Json::from("The request took too long and was abandoned."),
69 )]))
70 } else {
71 response.with_text("The request took too long and was abandoned.")
72 }
73 }
74 }
75 })
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::method::Method;
83 use crate::router::Router;
84 use crate::testing::TestClient;
85
86 fn client(limit: Duration) -> TestClient {
87 let mut router = Router::new();
88 router.middleware(Timeout::after(limit));
89 router.get("/fast", |_req: Request| async { Response::text("done") });
90 router.get("/slow", |_req: Request| async {
91 tokio::time::sleep(Duration::from_secs(30)).await;
92 Response::text("finally")
93 });
94 TestClient::new(router)
95 }
96
97 #[tokio::test]
98 async fn a_handler_within_the_limit_is_untouched() {
99 let response = client(Duration::from_secs(1)).get("/fast").await;
100 let response = response.assert_ok();
101 assert_eq!(response.body(), "done");
102 }
103
104 #[tokio::test]
105 async fn a_handler_over_the_limit_is_cut_off_with_a_503() {
106 let started = std::time::Instant::now();
107 let response = client(Duration::from_millis(50)).get("/slow").await;
108 assert!(started.elapsed() < Duration::from_secs(5), "the wait ended at the limit");
109 let response = response.assert_status(503);
110 assert!(response.body().contains("too long"));
111 }
112
113 #[tokio::test]
114 async fn an_api_client_gets_the_reason_as_json() {
115 let request = Request::new(Method::Get, "/slow").with_header("accept", "application/json");
116 let response = client(Duration::from_millis(50)).send(request).await;
117 let response = response.assert_status(503);
118 assert!(response.header("content-type").unwrap().starts_with("application/json"));
119 assert!(response.json().get("message").is_some());
120 }
121}