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