Skip to main content

soma_http_server/middleware/
timeout.rs

1//! Generic per-request timeout.
2//!
3//! Unlike plain `tower::timeout`, `tower_http`'s [`TimeoutLayer`] never
4//! fails the inner service — on elapse it directly returns a response with
5//! a caller-chosen status code, so it composes onto an Axum `Router` with
6//! no error-handling layer required. `tower_http` itself has no built-in
7//! default status code (its zero-arg constructor is deprecated in favor of
8//! `with_status_code`); [`timeout_layer`] below is this crate's own choice
9//! to default that code to `408 Request Timeout`.
10
11use std::time::Duration;
12
13use axum::http::StatusCode;
14pub use tower_http::timeout::TimeoutLayer;
15
16/// Build a layer that returns `408 Request Timeout` (this crate's chosen
17/// default — see the module docs) for any request taking longer than
18/// `duration`.
19pub fn timeout_layer(duration: Duration) -> TimeoutLayer {
20    TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, duration)
21}
22
23/// Build a layer that returns `status_code` for any request taking longer
24/// than `duration`.
25pub fn timeout_layer_with_status(status_code: StatusCode, duration: Duration) -> TimeoutLayer {
26    TimeoutLayer::with_status_code(status_code, duration)
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use axum::{body::Body, http::Request, routing::get, Router};
33    use tower::ServiceExt;
34
35    #[tokio::test]
36    async fn fast_handler_is_unaffected_by_a_generous_timeout() {
37        let app = Router::new()
38            .route("/", get(|| async { "ok" }))
39            .layer(timeout_layer(Duration::from_secs(30)));
40
41        let request = Request::builder().uri("/").body(Body::empty()).unwrap();
42        let response = app.oneshot(request).await.unwrap();
43        assert_eq!(response.status(), StatusCode::OK);
44    }
45
46    #[tokio::test]
47    async fn slow_handler_is_cut_off_by_the_timeout() {
48        let app = Router::new()
49            .route(
50                "/",
51                get(|| async {
52                    tokio::time::sleep(Duration::from_millis(200)).await;
53                    "too slow"
54                }),
55            )
56            .layer(timeout_layer(Duration::from_millis(20)));
57
58        let request = Request::builder().uri("/").body(Body::empty()).unwrap();
59        let response = app.oneshot(request).await.unwrap();
60        assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
61    }
62}