use crate::handler::BoxFuture;
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use crate::status::Status;
use rustlavel_core::Json;
use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct Timeout {
limit: Duration,
}
impl Timeout {
pub fn after(limit: Duration) -> Self {
Timeout { limit }
}
pub fn seconds(seconds: u64) -> Self {
Timeout::after(Duration::from_secs(seconds))
}
}
impl Middleware for Timeout {
fn handle(&self, request: Request, next: Next) -> BoxFuture<Response> {
let limit = self.limit;
let (method, path) = (request.method(), request.path().to_string());
let wants_json = request.wants_json();
Box::pin(async move {
match tokio::time::timeout(limit, next.run(request)).await {
Ok(response) => response,
Err(_) => {
rustlavel_core::warn!(
"{method} {path} did not finish within {} ms and was abandoned",
limit.as_millis()
);
let response = Response::new(Status::SERVICE_UNAVAILABLE);
if wants_json {
response.with_json(Json::object([(
"message",
Json::from("The request took too long and was abandoned."),
)]))
} else {
response.with_text("The request took too long and was abandoned.")
}
}
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::method::Method;
use crate::router::Router;
use crate::testing::TestClient;
fn client(limit: Duration) -> TestClient {
let mut router = Router::new();
router.middleware(Timeout::after(limit));
router.get("/fast", |_req: Request| async { Response::text("done") });
router.get("/slow", |_req: Request| async {
tokio::time::sleep(Duration::from_secs(30)).await;
Response::text("finally")
});
TestClient::new(router)
}
#[tokio::test]
async fn a_handler_within_the_limit_is_untouched() {
let response = client(Duration::from_secs(1)).get("/fast").await;
let response = response.assert_ok();
assert_eq!(response.body(), "done");
}
#[tokio::test]
async fn a_handler_over_the_limit_is_cut_off_with_a_503() {
let started = std::time::Instant::now();
let response = client(Duration::from_millis(50)).get("/slow").await;
assert!(started.elapsed() < Duration::from_secs(5), "the wait ended at the limit");
let response = response.assert_status(503);
assert!(response.body().contains("too long"));
}
#[tokio::test]
async fn an_api_client_gets_the_reason_as_json() {
let request = Request::new(Method::Get, "/slow").with_header("accept", "application/json");
let response = client(Duration::from_millis(50)).send(request).await;
let response = response.assert_status(503);
assert!(response.header("content-type").unwrap().starts_with("application/json"));
assert!(response.json().get("message").is_some());
}
}