Skip to main content

rws_server/timeout/
mod.rs

1//! Per-route request timeouts.
2//!
3//! A single global read timeout (30 s per connection, or `RWS_CONFIG_*`)
4//! applies uniformly to every route today. A file-upload endpoint may
5//! legitimately need 120 s while a health check must complete in 500 ms —
6//! there's no way to express that difference without wrapping a handler
7//! yourself. This module provides that wrapping.
8//!
9//! # Honest limitation: Rust cannot preempt a running synchronous call
10//!
11//! For synchronous handlers (`Router`, `AppWithState`, plain `Application`s),
12//! there is no safe way to forcibly stop a thread that's already running.
13//! [`with_timeout`], [`with_timeout_state`], and [`TimeoutLayer`] all run the
14//! wrapped work on a background thread and bound how long they *wait* for
15//! it — if the deadline passes, the caller gets a `504 Gateway Timeout`
16//! response immediately, but the background thread keeps running to
17//! completion (its result is simply discarded). This bounds the **client's**
18//! wait time, not the handler's actual resource usage.
19//!
20//! For genuine cancellation, use [`with_timeout_async`] with
21//! `AsyncAppWithState` (requires the `http2` feature): dropping a `Future`
22//! that hasn't finished actually stops its execution at the next `.await`
23//! point, backed by `tokio::time::timeout`.
24//!
25//! # Example — `Router`
26//!
27//! ```rust,no_run
28//! # extern crate rws_server as rust_web_server;
29//! use rust_web_server::router::Router;
30//! use rust_web_server::timeout::with_timeout;
31//! use rust_web_server::response::Response;
32//! use rust_web_server::core::New;
33//! use std::time::Duration;
34//!
35//! let router = Router::new()
36//!     .get("/healthz", with_timeout(Duration::from_millis(500), |_req, _params, _conn| Response::new()))
37//!     .post("/upload", with_timeout(Duration::from_secs(120), |_req, _params, _conn| Response::new()));
38//! ```
39//!
40//! # Example — wrapping a whole `Application`
41//!
42//! ```rust,no_run
43//! # extern crate rws_server as rust_web_server;
44//! use rust_web_server::app::App;
45//! use rust_web_server::core::New;
46//! use rust_web_server::timeout::TimeoutLayer;
47//! use std::time::Duration;
48//!
49//! let app = TimeoutLayer::new(App::new(), Duration::from_secs(5));
50//! ```
51
52#[cfg(test)]
53mod tests;
54
55use std::sync::mpsc;
56use std::sync::Arc;
57use std::thread;
58use std::time::Duration;
59
60use crate::application::Application;
61use crate::request::Request;
62use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
63use crate::router::PathParams;
64use crate::server::ConnectionInfo;
65
66/// Runs `compute` on a detached background thread. Returns `Some(result)` if
67/// it finishes within `duration`, `None` otherwise — in which case the
68/// thread keeps running to completion; its result is dropped when it
69/// eventually sends on the (by-then-abandoned) channel.
70fn run_with_timeout<T, F>(duration: Duration, compute: F) -> Option<T>
71where
72    T: Send + 'static,
73    F: FnOnce() -> T + Send + 'static,
74{
75    let (tx, rx) = mpsc::channel();
76    thread::spawn(move || {
77        let _ = tx.send(compute());
78    });
79    rx.recv_timeout(duration).ok()
80}
81
82fn timeout_response() -> Response {
83    crate::error::problem(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout, "")
84}
85
86/// Wraps a stateless handler (the `Router` handler signature) so it must
87/// complete within `duration` or the caller gets `504 Gateway Timeout`
88/// instead of waiting further. See the [module docs](self) for the
89/// cancellation caveat.
90pub fn with_timeout<F>(
91    duration: Duration,
92    handler: F,
93) -> impl Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static
94where
95    F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static,
96{
97    let handler = Arc::new(handler);
98    move |req, params, conn| {
99        let handler = Arc::clone(&handler);
100        let req = req.clone();
101        let params = params.clone();
102        let conn = conn.clone();
103        run_with_timeout(duration, move || handler(&req, &params, &conn))
104            .unwrap_or_else(timeout_response)
105    }
106}
107
108/// Wraps an `AppWithState<S>` handler (which additionally receives `&S`) so
109/// it must complete within `duration` or the caller gets `504 Gateway
110/// Timeout` instead of waiting further.
111///
112/// Requires `S: Clone` — the wrapped call runs on a background thread that
113/// needs its own owned copy of the state, since the handler signature only
114/// gives a borrowed `&S`. Most `AppWithState` state types hold their own
115/// data behind `Arc` internally and are cheap to `#[derive(Clone)]`; if
116/// yours isn't, wrap the whole app with [`TimeoutLayer`] instead, or switch
117/// the route to `AsyncAppWithState` and use [`with_timeout_async`], which
118/// needs no `Clone` bound at all.
119pub fn with_timeout_state<S, F>(
120    duration: Duration,
121    handler: F,
122) -> impl Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static
123where
124    S: Clone + Send + Sync + 'static,
125    F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
126{
127    let handler = Arc::new(handler);
128    move |req, params, conn, state: &S| {
129        let handler = Arc::clone(&handler);
130        let req = req.clone();
131        let params = params.clone();
132        let conn = conn.clone();
133        let state = state.clone();
134        run_with_timeout(duration, move || handler(&req, &params, &conn, &state))
135            .unwrap_or_else(timeout_response)
136    }
137}
138
139/// Wraps any owned [`Application`] (or a shared `Arc<dyn Application>`) so
140/// every request through it must complete within `duration` or the client
141/// gets `504 Gateway Timeout` instead of waiting further.
142///
143/// Use this to put one blanket timeout around an entire `App`/`AppWithState`/
144/// custom `Application`. For different timeouts on different routes within
145/// the same app, use [`with_timeout`] / [`with_timeout_state`] /
146/// [`with_timeout_async`] on individual handlers instead.
147pub struct TimeoutLayer<A: ?Sized> {
148    inner: Arc<A>,
149    duration: Duration,
150}
151
152impl<A: Application + Send + Sync + 'static> TimeoutLayer<A> {
153    /// Wrap an owned application.
154    pub fn new(inner: A, duration: Duration) -> Self {
155        TimeoutLayer { inner: Arc::new(inner), duration }
156    }
157}
158
159impl<A: Application + Send + Sync + ?Sized + 'static> TimeoutLayer<A> {
160    /// Wrap an already-shared application (e.g. `Arc<dyn Application + Send + Sync>`).
161    pub fn from_arc(inner: Arc<A>, duration: Duration) -> Self {
162        TimeoutLayer { inner, duration }
163    }
164}
165
166impl<A: Application + Send + Sync + ?Sized + 'static> Application for TimeoutLayer<A> {
167    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
168        let inner = Arc::clone(&self.inner);
169        let request = request.clone();
170        let connection = connection.clone();
171        match run_with_timeout(self.duration, move || inner.execute(&request, &connection)) {
172            Some(result) => result,
173            None => Ok(timeout_response()),
174        }
175    }
176
177    fn registered_routes(&self) -> Vec<crate::router::RouteInfo> {
178        self.inner.registered_routes()
179    }
180
181    fn middleware_names(&self) -> Vec<&'static str> {
182        let mut names = vec!["TimeoutLayer"];
183        names.extend(self.inner.middleware_names());
184        names
185    }
186}
187
188#[cfg(feature = "http2")]
189mod async_timeout {
190    use std::future::Future;
191    use std::pin::Pin;
192    use std::sync::Arc;
193    use std::time::Duration;
194
195    use crate::request::Request;
196    use crate::response::Response;
197    use crate::router::PathParams;
198    use crate::server::ConnectionInfo;
199
200    type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
201
202    /// Wraps an `AsyncAppWithState<S>` handler so its future is dropped —
203    /// genuinely cancelled at its next `.await` point — if it doesn't
204    /// resolve within `duration`. Backed by `tokio::time::timeout`; requires
205    /// the `http2` feature. No `Clone` bound on `S`: `AsyncAppWithState`
206    /// already passes state as an owned `Arc<S>`.
207    pub fn with_timeout_async<S, F, Fut>(
208        duration: Duration,
209        handler: F,
210    ) -> impl Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> BoxFuture<Response> + Send + Sync + 'static
211    where
212        S: Send + Sync + 'static,
213        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
214        Fut: Future<Output = Response> + Send + 'static,
215    {
216        move |req, params, conn, state| {
217            let fut = handler(req, params, conn, state);
218            Box::pin(async move {
219                match tokio::time::timeout(duration, fut).await {
220                    Ok(response) => response,
221                    Err(_) => super::timeout_response(),
222                }
223            })
224        }
225    }
226}
227
228#[cfg(feature = "http2")]
229pub use async_timeout::with_timeout_async;