Skip to main content

rustlavel_http/
timeout.rs

1//! A ceiling on how long a handler may run.
2//!
3//! Without one, a handler waiting on a database that has stopped answering
4//! holds its connection, its task and its client for as long as the client is
5//! willing to wait — which for a browser is minutes, and for a retrying
6//! service is forever. With one, the wait ends at a known point and the
7//! client gets an answer it can act on.
8//!
9//! ```ignore
10//! r.group("/api", |api| {
11//!     api.middleware(Timeout::after(Duration::from_secs(10)));
12//!     …
13//! });
14//! ```
15//!
16//! The response is a 503. Not 504, which is for a gateway whose *upstream*
17//! was slow, and not 408, which tells the client *it* was slow to send. A 503
18//! says the service could not answer in time, which is the truth, and carries
19//! no `Retry-After`, because nothing here knows when it would be safe to.
20//!
21//! What times out is dropped. A handler half-way through a database write is
22//! abandoned at whatever `.await` it was parked on; the write either landed
23//! or it did not, and the connection goes back to the pool in the state the
24//! driver leaves it. Keep the limit generous enough that only a genuinely
25//! stuck request hits it.
26
27use 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}