actus_server/server.rs
1//! [`Server`] — the hyper-based HTTP server that owns the request lifecycle:
2//! routing, body limiting, the middleware chain, CORS, compression, and the
3//! HTTP-correctness stamping (`Allow`, `Vary`), with graceful shutdown.
4
5#[cfg(feature = "compression")]
6use crate::compression::CompressionLayer;
7use crate::cors::CorsLayer;
8use crate::error::ServerError;
9use crate::middleware::{Middleware, MiddlewareChain, Outcome};
10use crate::request::Request;
11use crate::router::Router;
12#[cfg(feature = "websocket")]
13use crate::websocket;
14#[cfg(feature = "websocket")]
15use actus_reply::ProblemDetails;
16use actus_reply::{Finalizer, ReplyData, WebError};
17use bytes::Bytes;
18#[cfg(feature = "websocket")]
19use http::{HeaderValue, StatusCode, header};
20use http_body_util::combinators::BoxBody;
21use hyper::body::Incoming;
22use hyper::service::service_fn;
23use hyper::{Request as HyperRequest, Response as HyperResponse};
24use std::future::Future;
25use std::net::SocketAddr;
26use std::sync::Arc;
27use std::time::Duration;
28use tokio::net::TcpListener;
29use tokio::sync::Semaphore;
30use tokio::task::JoinSet;
31use tracing::{Instrument, Level, error, info, span, warn};
32
33type ResponseBody = BoxBody<Bytes, WebError>;
34
35/// One kibibyte (1024 bytes). For readable byte-size limits, e.g.
36/// `#[controller(max_body_bytes = 4 * KIB)]`.
37pub const KIB: usize = 1024;
38/// One mebibyte (1024 × 1024 bytes), e.g. `Server::with_max_body_bytes(2 * MIB)`.
39pub const MIB: usize = 1024 * KIB;
40/// One gibibyte (1024 × 1024 × 1024 bytes).
41pub const GIB: usize = 1024 * MIB;
42
43/// Default cap on the request body Actus will buffer: **2 MiB** — a safe
44/// ceiling for the common case (JSON APIs, forms). Endpoints that accept larger
45/// bodies (uploads) opt in via [`Server::with_max_body_bytes`] or a
46/// per-controller `#[controller(max_body_bytes = …)]`. Matches axum's default.
47pub const DEFAULT_MAX_BODY_BYTES: usize = 2 * MIB;
48
49/// Default grace period for in-flight connections to finish after a
50/// shutdown signal: 30 seconds. Override with [`Server::with_drain_deadline`].
51pub const DEFAULT_DRAIN_DEADLINE: Duration = Duration::from_secs(30);
52
53/// The main Actus server.
54pub struct Server {
55 router: Arc<Router>,
56 middleware_chain: Arc<MiddlewareChain>,
57 finalizer: Arc<Finalizer>,
58 max_body_bytes: usize,
59 cors: Option<Arc<CorsLayer>>,
60 #[cfg(feature = "compression")]
61 compression: Option<Arc<CompressionLayer>>,
62 /// `Some(d)` caps each request's total time (parse → middleware →
63 /// handler → after-chain → finalize) at `d`; an over-budget request is
64 /// aborted and replied with `504 Gateway Timeout`. `None` disables the
65 /// per-request timer (the default).
66 request_timeout: Option<Duration>,
67 /// Grace period for in-flight connections to drain after shutdown.
68 drain_deadline: Duration,
69 /// Cap on concurrent connection tasks. `Some(n)` installs an
70 /// `Arc<Semaphore>` of `n` permits in the accept loop; while at
71 /// capacity, the loop pauses on permit acquisition and new SYNs queue
72 /// in the kernel's accept backlog (`SOMAXCONN`), at which point the
73 /// kernel drops them. `None` is unbounded.
74 max_connections: Option<usize>,
75 /// Cap on the total bytes being buffered across all in-flight body
76 /// reads. `Some(n)` installs a byte-permit semaphore; each
77 /// `collect_body_capped` reserves its per-request cap upfront and
78 /// releases the permits when the body is buffered or rejected. Refuses
79 /// excess requests with `503 Service Unavailable` (via `WebError::Busy`).
80 /// `None` is unbounded.
81 max_inflight_body_bytes: Option<Arc<Semaphore>>,
82 /// `Some(d)` is forwarded to hyper's `http1::Builder::header_read_timeout`
83 /// — bounds how long after starting to read request headers we'll wait
84 /// before dropping the connection. Catches slowloris and clients that
85 /// TCP-connect-and-send-nothing. `None` leaves hyper's default (none).
86 header_read_timeout: Option<Duration>,
87}
88
89impl Server {
90 /// Create a server for `router` with default settings: no middleware, no
91 /// CORS, the default body cap, and no DoS limits. Configure it with the
92 /// `with_*` builder methods, then call [`run`](Self::run).
93 pub fn new(router: Router) -> Self {
94 Self {
95 router: Arc::new(router),
96 middleware_chain: Arc::new(MiddlewareChain::new()),
97 finalizer: Arc::new(Finalizer::new()),
98 max_body_bytes: DEFAULT_MAX_BODY_BYTES,
99 cors: None,
100 #[cfg(feature = "compression")]
101 compression: None,
102 request_timeout: None,
103 drain_deadline: DEFAULT_DRAIN_DEADLINE,
104 max_connections: None,
105 max_inflight_body_bytes: None,
106 header_read_timeout: None,
107 }
108 }
109
110 /// The router this server serves, shared.
111 ///
112 /// Lets application code keep using the route tree after `Server::new`
113 /// has consumed it — most usefully from a `Middleware` that wants the
114 /// framework's own longest-prefix matcher instead of re-deriving it. A
115 /// declaration-keyed gate is the motivating case:
116 ///
117 /// ```ignore
118 /// let server = Server::new(router);
119 /// let router = server.router(); // Arc<Router>
120 /// let server = server.with_middleware(FloorGate { router });
121 /// // …in the gate's `before`:
122 /// // if let Some(rm) = self.router.match_controller(&req.path_parts) {
123 /// // if rm.controller.actus_expects() == Some("credential") && … { … }
124 /// // }
125 /// ```
126 ///
127 /// The clone is an `Arc` clone; a gate's extra `match_controller` call is
128 /// one map lookup per path segment on top of the one the server already
129 /// does. See the README's "Route families" section for the full pattern.
130 pub fn router(&self) -> Arc<Router> {
131 Arc::clone(&self.router)
132 }
133
134 /// Adds a middleware to the server's request processing chain.
135 pub fn with_middleware(mut self, middleware: impl Middleware + 'static) -> Self {
136 let mut chain = Arc::try_unwrap(self.middleware_chain).unwrap_or_else(|arc| (*arc).clone());
137 chain.add(middleware);
138 self.middleware_chain = Arc::new(chain);
139 self
140 }
141
142 /// Enables CORS with the given policy. The server then answers preflight
143 /// (`OPTIONS`) requests itself and adds the `Access-Control-*` headers to
144 /// every cross-origin response (including error responses). See
145 /// [`CorsLayer`].
146 pub fn with_cors(mut self, cors: CorsLayer) -> Self {
147 self.cors = Some(Arc::new(cors));
148 self
149 }
150
151 /// Enables response compression (gzip / brotli). For each response Actus
152 /// picks an encoding from the request's `Accept-Encoding` and compresses
153 /// buffered, compressible bodies above the layer's size threshold. See
154 /// [`CompressionLayer`]. *(Requires the `compression` feature.)*
155 #[cfg(feature = "compression")]
156 pub fn with_compression(mut self, layer: CompressionLayer) -> Self {
157 self.compression = Some(Arc::new(layer));
158 self
159 }
160
161 /// Caps the request body Actus will buffer (default
162 /// [`DEFAULT_MAX_BODY_BYTES`] = 2 MiB). A larger body is rejected with
163 /// `413 Payload Too Large` before it can exhaust memory — the limit
164 /// bounds buffered bytes, so it also covers chunked bodies that lie about
165 /// (or omit) `Content-Length`.
166 ///
167 /// `0` is accepted and means "reject every non-empty body" — typically
168 /// only useful on a strictly-GET surface that should never see a body.
169 pub fn with_max_body_bytes(mut self, max: usize) -> Self {
170 self.max_body_bytes = max;
171 self
172 }
173
174 /// Cap the total time any single request may take — body parse,
175 /// middleware `before`, handler, middleware `after`, and finalization
176 /// combined. An over-budget request is aborted (the handler's future
177 /// is dropped) and the client gets `504 Gateway Timeout`. No timeout
178 /// is set by default.
179 ///
180 /// **Scope.** The timer covers the request/response exchange. A
181 /// WebSocket upgrade succeeds inside the timer (the `101` is the
182 /// response); the post-upgrade conversation runs in its own task and
183 /// is not bound by this timeout.
184 ///
185 /// **Effect of an over-budget request.** When the timer elapses the
186 /// in-flight future is dropped, which cancels whatever the handler
187 /// was awaiting (DB query, channel recv, etc.). The 504 reply is
188 /// one-shot — the after-chain doesn't run on it (by definition,
189 /// some component upstream was unresponsive; running more risks
190 /// hanging again).
191 pub fn with_request_timeout(mut self, d: Duration) -> Self {
192 self.request_timeout = Some(d);
193 self
194 }
195
196 /// Override the grace period for in-flight connections after a
197 /// shutdown signal (default [`DEFAULT_DRAIN_DEADLINE`] = 30 s).
198 /// Anything still running at the deadline is hard-aborted via
199 /// `JoinSet::shutdown`. Use a longer value for surfaces that hold
200 /// long-lived connections (large file downloads, WebSockets);
201 /// a shorter value for fast-iteration dev workflows. `Duration::ZERO`
202 /// aborts every in-flight task immediately.
203 pub fn with_drain_deadline(mut self, d: Duration) -> Self {
204 self.drain_deadline = d;
205 self
206 }
207
208 /// Cap concurrent connection tasks at `n`. While the cap is held, the
209 /// accept loop pauses on permit acquisition; new SYNs queue in the
210 /// kernel's accept backlog and (once that fills, governed by
211 /// `SOMAXCONN`) get dropped at the OS level. No userland reject /
212 /// no-503-per-conn cost — the kernel handles the spillover.
213 ///
214 /// Each connection task holds its permit until it ends, including the
215 /// post-handshake WebSocket conversation. Size accordingly: a
216 /// `with_max_connections(N)` server can hold `N` long-lived WebSockets
217 /// *before* it stops accepting new connections of any kind.
218 ///
219 /// Unbounded by default (no semaphore installed).
220 pub fn with_max_connections(mut self, n: usize) -> Self {
221 self.max_connections = Some(n);
222 self
223 }
224
225 /// Cap the total bytes being buffered across all in-flight body reads
226 /// at `n`. Each request reserves its per-request cap (see
227 /// `with_max_body_bytes`) from this global budget upfront; if the
228 /// budget is exhausted, the request is refused with `503 Service
229 /// Unavailable` (via [`WebError::Busy`]) and a short `Retry-After`.
230 ///
231 /// Together with [`Self::with_max_connections`] this puts a hard
232 /// ceiling on the framework's memory under adversarial load:
233 /// `with_max_connections(C) * with_max_body_bytes(B)` is the *worst*
234 /// case absent this knob; with it, the ceiling is `min(C * B, this
235 /// value)`.
236 ///
237 /// Pre-reserving the per-request cap over-counts (a 1 KB request
238 /// reserves up to its full cap); the alternative — incremental
239 /// per-chunk byte accounting — is more code for the same effective
240 /// ceiling, and a request that has already started buffering can't
241 /// be sensibly aborted partway through anyway.
242 ///
243 /// `n` is clamped to `u32::MAX` internally (Tokio's `Semaphore`
244 /// permit count uses `u32`); for practical deployments this is no
245 /// limit (4 GiB).
246 ///
247 /// Unbounded by default.
248 pub fn with_max_inflight_body_bytes(mut self, n: usize) -> Self {
249 // u32 cap is a tokio Semaphore constraint, not a design choice.
250 let n_capped = n.min(u32::MAX as usize);
251 self.max_inflight_body_bytes = Some(Arc::new(Semaphore::new(n_capped)));
252 self
253 }
254
255 /// Bound how long after starting to read request headers we'll wait
256 /// before dropping the connection. Forwards to hyper's
257 /// `http1::Builder::header_read_timeout`. Catches slowloris (sending
258 /// headers one byte at a time) and clients that TCP-connect-and-send-
259 /// nothing — the most common file-descriptor-exhaustion attack on a
260 /// keep-alive HTTP server.
261 ///
262 /// Note: hyper 1.x doesn't have a separate "idle between requests"
263 /// timeout (after a complete request, an idle keep-alive connection
264 /// stays open until either side closes or the OS-level TCP keep-alive
265 /// fires). If that matters for your deployment, either disable
266 /// keep-alive entirely upstream of Actus or rely on the OS knobs.
267 ///
268 /// No timeout by default (hyper's default).
269 pub fn with_header_read_timeout(mut self, d: Duration) -> Self {
270 self.header_read_timeout = Some(d);
271 self
272 }
273
274 /// Runs the server on `127.0.0.1:port` (loopback only). For a different
275 /// bind address — e.g. `0.0.0.0:port` to accept connections from other
276 /// hosts in a container — use [`Server::run_on`].
277 ///
278 /// Listens for SIGTERM/SIGINT (Unix) or Ctrl-C (cross-platform) and
279 /// shuts down gracefully: stops accepting new connections, signals
280 /// in-flight connections to finish, and waits up to 30 seconds for
281 /// them to drain before returning.
282 pub async fn run(self, port: u16) -> Result<(), ServerError> {
283 self.run_on(SocketAddr::from(([127, 0, 0, 1], port))).await
284 }
285
286 /// Like [`Server::run`] but binds an arbitrary address. Pass
287 /// `0.0.0.0:port` (or `[::]:port`) to accept connections from other hosts.
288 pub async fn run_on(self, addr: SocketAddr) -> Result<(), ServerError> {
289 self.run_with_shutdown_on(addr, default_shutdown_signal())
290 .await
291 }
292
293 /// Like [`Server::run`] but with a custom shutdown trigger (a future that,
294 /// when it resolves, starts the graceful drain). Binds `127.0.0.1:port`;
295 /// see [`Server::run_with_shutdown_on`] for a custom bind address. Useful
296 /// for tests or for embedding the server in a larger supervision tree.
297 pub async fn run_with_shutdown(
298 self,
299 port: u16,
300 shutdown: impl Future<Output = ()> + Send + 'static,
301 ) -> Result<(), ServerError> {
302 self.run_with_shutdown_on(SocketAddr::from(([127, 0, 0, 1], port)), shutdown)
303 .await
304 }
305
306 /// Bind `addr`, then serve exactly as
307 /// [`Server::run_with_shutdown_listener`] — the general form — does.
308 /// [`Server::run`], [`Server::run_on`], and [`Server::run_with_shutdown`]
309 /// are thin wrappers over this; this is a thin bind over the listener
310 /// form.
311 ///
312 /// **Drain bound.** Once `shutdown` resolves the server stops accepting
313 /// and signals every in-flight connection to wind down. The drain
314 /// deadline defaults to [`DEFAULT_DRAIN_DEADLINE`] (30 s); override
315 /// with [`Server::with_drain_deadline`]. Anything still running at
316 /// the deadline is hard-aborted via `JoinSet::shutdown`. In particular,
317 /// long-lived connections (WebSockets, slow downloads, kept-alive idle
318 /// clients) and any connection task that raced the shutdown notification
319 /// and missed it both get aborted at the deadline rather than draining
320 /// gracefully.
321 pub async fn run_with_shutdown_on(
322 self,
323 addr: SocketAddr,
324 shutdown: impl Future<Output = ()> + Send + 'static,
325 ) -> Result<(), ServerError> {
326 let listener = TcpListener::bind(addr).await?;
327 self.run_with_shutdown_listener(listener, shutdown).await
328 }
329
330 /// Like [`Server::run_on`] but serves on a listener the caller already
331 /// bound (or inherited), with the default SIGTERM / SIGINT shutdown
332 /// trigger. See [`Server::run_with_shutdown_listener`] for what a
333 /// pre-bound listener buys and how to adopt an inherited one.
334 pub async fn run_listener(self, listener: TcpListener) -> Result<(), ServerError> {
335 self.run_with_shutdown_listener(listener, default_shutdown_signal())
336 .await
337 }
338
339 /// The most general form: serve on a listener the caller already bound
340 /// (or inherited) until `shutdown` resolves, then drain. Every other
341 /// `run*` method is a wrapper over this one.
342 ///
343 /// **Why hand the server a listener.** Two cases the bind-it-yourself
344 /// forms can't express:
345 ///
346 /// - **Socket activation** — a supervisor (systemd's `LISTEN_FDS`
347 /// protocol, launchd, …) owns the listening socket and passes it to
348 /// the process it spawns. Because the socket outlives the process,
349 /// connections arriving during a restart queue in the kernel's accept
350 /// backlog instead of being refused, and the next process serves them.
351 /// - **Race-free embedding and tests** — bind `127.0.0.1:0`, keep the
352 /// listener, and pass it in; no bind-drop-rebind window in which the
353 /// port can be lost, and requests may connect before the accept loop
354 /// even starts (the kernel queues them).
355 ///
356 /// An inherited `std::net::TcpListener` must be set non-blocking before
357 /// conversion — tokio requires it, and a supervisor passes the fd
358 /// blocking:
359 ///
360 /// ```no_run
361 /// # async fn doc(server: actus_server::Server, inherited: std::net::TcpListener)
362 /// # -> Result<(), actus_server::ServerError> {
363 /// inherited.set_nonblocking(true)?;
364 /// let listener = tokio::net::TcpListener::from_std(inherited)?;
365 /// server.run_with_shutdown_listener(listener, std::future::pending()).await
366 /// # }
367 /// ```
368 ///
369 /// **Drain bound.** As [`Server::run_with_shutdown_on`]: once `shutdown`
370 /// resolves the server stops accepting (the listener is dropped — under
371 /// socket activation the *socket* stays open in the supervisor, which is
372 /// the point) and in-flight connections get [`Server::with_drain_deadline`]
373 /// (default [`DEFAULT_DRAIN_DEADLINE`] = 30 s) to finish before being
374 /// aborted. A streaming response that never completes (SSE) always rides
375 /// to the deadline.
376 pub async fn run_with_shutdown_listener(
377 self,
378 listener: TcpListener,
379 shutdown: impl Future<Output = ()> + Send + 'static,
380 ) -> Result<(), ServerError> {
381 match listener.local_addr() {
382 Ok(addr) => info!("Server listening on http://{}", addr),
383 // An inherited fd can decline local_addr (exotic socket family);
384 // serving still works, so log what we know and carry on.
385 Err(_) => info!("Server listening on an inherited socket"),
386 }
387
388 let app = Arc::new(self);
389 // Per-connection cancellation: once `Notify::notify_waiters` fires,
390 // every in-flight task wakes up and asks hyper to gracefully close
391 // its connection (finishing the current response, then exiting).
392 let notify = Arc::new(tokio::sync::Notify::new());
393 let mut tasks: JoinSet<()> = JoinSet::new();
394
395 // Optional cap on concurrent connections. When at-capacity the
396 // accept loop pauses on permit acquisition; new SYNs queue in the
397 // kernel accept backlog (SOMAXCONN) and get dropped at the OS
398 // level once that fills. Each spawned connection task moves its
399 // permit in; the permit releases when the task exits.
400 let conn_permits = app.max_connections.map(|n| Arc::new(Semaphore::new(n)));
401
402 tokio::pin!(shutdown);
403
404 loop {
405 tokio::select! {
406 // Accept-branch: acquire a connection permit first (when
407 // a cap is configured), then accept. The outer select
408 // races this against shutdown so a paused-at-capacity
409 // accept loop still notices the shutdown signal.
410 accept_with_permit = async {
411 let permit = match &conn_permits {
412 Some(s) => Some(s.clone().acquire_owned().await.expect("semaphore never closed")),
413 None => None,
414 };
415 let result = listener.accept().await;
416 (result, permit)
417 } => {
418 let (accept_result, permit) = accept_with_permit;
419 let (stream, _peer) = match accept_result {
420 Ok(s) => s,
421 Err(e) => {
422 error!("accept error: {}", e);
423 // permit released when this branch ends — fine
424 continue;
425 }
426 };
427 let io = hyper_util::rt::TokioIo::new(stream);
428 let app = app.clone();
429 let notify = notify.clone();
430 let header_timeout = app.header_read_timeout;
431 tasks.spawn(async move {
432 // The permit (if any) lives for the connection's
433 // lifetime; releasing happens at task drop.
434 let _permit = permit;
435
436 let mut builder = hyper::server::conn::http1::Builder::new();
437 if let Some(d) = header_timeout {
438 builder.header_read_timeout(d);
439 }
440 let conn = builder.serve_connection(
441 io,
442 service_fn(move |req| app.clone().handle_request(req)),
443 );
444 // With the `websocket` feature, allow `101 Switching
445 // Protocols` responses to hand off the connection.
446 #[cfg(feature = "websocket")]
447 let conn = conn.with_upgrades();
448 tokio::pin!(conn);
449 tokio::select! {
450 res = conn.as_mut() => {
451 if let Err(err) = res {
452 error!("Error serving connection: {}", err);
453 }
454 }
455 _ = notify.notified() => {
456 conn.as_mut().graceful_shutdown();
457 if let Err(err) = conn.await {
458 error!("Error during graceful shutdown: {}", err);
459 }
460 }
461 }
462 });
463 }
464 // Reap finished connection tasks so the `JoinSet` doesn't grow
465 // without bound over the server's lifetime — and so a panicked
466 // connection task is logged promptly, not only at shutdown.
467 joined = tasks.join_next(), if !tasks.is_empty() => {
468 match joined {
469 Some(Err(e)) if e.is_panic() => error!("Connection task panicked: {}", e),
470 Some(Err(e)) => error!("Connection task failed: {}", e),
471 Some(Ok(())) | None => {}
472 }
473 }
474 _ = &mut shutdown => {
475 info!("Shutdown signal received; draining in-flight requests");
476 break;
477 }
478 }
479 }
480
481 // Stop accepting; signal connections to wind down.
482 drop(listener);
483 notify.notify_waiters();
484
485 // Drain. The grace period is configurable via
486 // `Server::with_drain_deadline` (default 30 s).
487 let drain_deadline = tokio::time::sleep(app.drain_deadline);
488 tokio::pin!(drain_deadline);
489 loop {
490 tokio::select! {
491 next = tasks.join_next() => {
492 match next {
493 Some(Ok(())) => {}
494 Some(Err(e)) if e.is_panic() => {
495 error!("Connection task panicked: {}", e);
496 }
497 Some(Err(e)) => {
498 error!("Connection task failed: {}", e);
499 }
500 None => break,
501 }
502 }
503 _ = &mut drain_deadline => {
504 warn!("Drain deadline exceeded; aborting {} connection(s)", tasks.len());
505 tasks.shutdown().await;
506 break;
507 }
508 }
509 }
510
511 info!("Server shutdown complete");
512 Ok(())
513 }
514
515 /// Stamp the configured CORS response headers onto `response` (no-op when
516 /// CORS isn't enabled, or the request had no allowed `Origin`). Applied to
517 /// *every* outgoing response — success and error alike — so the browser
518 /// can read 4xx/5xx bodies.
519 fn with_cors_headers(
520 &self,
521 request: &Request,
522 mut response: HyperResponse<ResponseBody>,
523 ) -> HyperResponse<ResponseBody> {
524 if let Some(cors) = &self.cors {
525 cors.apply(&request.headers, response.headers_mut(), false);
526 }
527 response
528 }
529
530 /// Fulfil a `ReplyData::Upgrade` from a handler when the request was
531 /// genuinely a WebSocket handshake: send `101 Switching Protocols` and
532 /// spawn the handler on the upgraded connection. (The "handler returned
533 /// Upgrade but the request wasn't a handshake" case is rewritten to a
534 /// 426 reply in [`Self::finalize_reply`] before reaching this method, so
535 /// it can flow through the after-chain like any other error.)
536 ///
537 /// No CORS headers on the `101`: WebSocket handshakes are scoped by
538 /// browser origin checks (the handler inspects `Origin` itself before
539 /// calling `ws::upgrade`), not by the CORS protocol — `Access-Control-*`
540 /// on a `101` is meaningless to the browser.
541 #[cfg(feature = "websocket")]
542 async fn complete_ws_upgrade(
543 &self,
544 handler: Box<dyn std::any::Any + Send>,
545 ws_upgrade: (hyper::upgrade::OnUpgrade, HeaderValue),
546 ) -> HyperResponse<ResponseBody> {
547 // `ReplyData::Upgrade` is only constructible via `ws::upgrade(...)`,
548 // which always boxes an `UpgradeTask`. A failing downcast would mean
549 // a crate-internal invariant is broken; surface as a panic rather
550 // than silently producing a 500.
551 let task = handler
552 .downcast::<websocket::UpgradeTask>()
553 .expect("ReplyData::Upgrade always carries an UpgradeTask");
554 let (on_upgrade, accept) = ws_upgrade;
555 tokio::spawn(websocket::run_upgrade(on_upgrade, *task));
556 let mut resp = self.finalizer.build_response(ReplyData::Empty).await;
557 *resp.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
558 let h = resp.headers_mut();
559 h.insert(header::CONNECTION, HeaderValue::from_static("upgrade"));
560 h.insert(header::UPGRADE, HeaderValue::from_static("websocket"));
561 h.insert(header::SEC_WEBSOCKET_ACCEPT, accept);
562 resp
563 }
564
565 /// Build the error reply for `error` and route it through
566 /// [`finalize_reply`](Self::finalize_reply), so the after-chain,
567 /// compression, and CORS apply to errors exactly as they do to handler
568 /// successes. This is the canonical way to produce a `WebError`
569 /// response anywhere a `Request` exists.
570 async fn finalize_error(
571 &self,
572 error: WebError,
573 request: &Request,
574 #[cfg(feature = "websocket")] ws_upgrade: Option<(hyper::upgrade::OnUpgrade, HeaderValue)>,
575 ) -> HyperResponse<ResponseBody> {
576 let data = self.finalizer.error_to_reply(error);
577 self.finalize_reply(
578 data,
579 request,
580 #[cfg(feature = "websocket")]
581 ws_upgrade,
582 )
583 .await
584 }
585
586 /// Run the after-middleware chain, then turn the reply into a `Response`
587 /// via [`dispatch_reply`](Self::dispatch_reply).
588 ///
589 /// **After-chain runs on every reply with a body and a `Request`.** That
590 /// includes handler successes, `Outcome::Respond` short-circuits, *and*
591 /// every error the application produced (404 / 405 / 401 / 400 / a
592 /// handler-returned `Err(WebError)`, etc.). The README's promise that a
593 /// request-id stamper "still fires on a short-circuit" generalizes to
594 /// every reply — that's the contract this method enforces.
595 ///
596 /// **Exceptions** (the after-chain *doesn't* run):
597 /// - **101 Switching Protocols** — a WebSocket-handshake success has no
598 /// HTTP body to decorate, and the upgrade machinery consumes the
599 /// connection.
600 /// - **Pre-parse failures** — a request that fails before
601 /// [`Request::from_hyper`] returns a skeleton (e.g. malformed HTTP
602 /// from hyper itself) has no `Request` to give the hook. The body-cap
603 /// 413 and truncated-body 400 are *not* exceptions here: `from_hyper`
604 /// now returns a skeleton `Request` on those, so they do run through
605 /// the after-chain.
606 /// - **CORS preflight 204** — synthesized before middleware or routing;
607 /// not an application request (see [`Self::handle_request`]).
608 async fn finalize_reply(
609 &self,
610 #[allow(unused_mut)] mut data: ReplyData,
611 request: &Request,
612 #[cfg(feature = "websocket")] ws_upgrade: Option<(hyper::upgrade::OnUpgrade, HeaderValue)>,
613 ) -> HyperResponse<ResponseBody> {
614 // If the handler returned `ws::upgrade(...)` but the request isn't a
615 // real WebSocket handshake, rewrite to a 426 error reply *here* so
616 // it flows through the same after-chain / compression / CORS path as
617 // any other error. Only the success-handshake path (Upgrade reply +
618 // ws_upgrade present) keeps the after-chain bypass — a 101 has no
619 // HTTP body to decorate.
620 #[cfg(feature = "websocket")]
621 if matches!(data, ReplyData::Upgrade(_)) && ws_upgrade.is_none() {
622 data = self.finalizer.error_to_reply(WebError::Problem(
623 ProblemDetails::new(StatusCode::UPGRADE_REQUIRED, "WebSocket Upgrade Required")
624 .detail("this endpoint expects a WebSocket handshake"),
625 ));
626 }
627
628 let needs_after_chain = !matches!(data, ReplyData::Upgrade(_));
629 if needs_after_chain
630 && let Err(e) = self
631 .middleware_chain
632 .process_response(request, &mut data)
633 .await
634 {
635 // After-chain itself errored. Build a plain error response
636 // (no further after-chain — recursion prevention) so a buggy
637 // hook can't infinite-loop the request.
638 return self.with_cors_headers(request, self.finalizer.build_error(e).await);
639 }
640 self.dispatch_reply(
641 data,
642 request,
643 #[cfg(feature = "websocket")]
644 ws_upgrade,
645 )
646 .await
647 }
648
649 /// Turn a handler's (or a short-circuiting middleware's) `ReplyData` into a
650 /// fully processed response: WebSocket upgrade if it's an `Upgrade` reply;
651 /// otherwise compress (if enabled), finalize, and stamp CORS / `Vary`.
652 async fn dispatch_reply(
653 &self,
654 #[allow(unused_mut)] mut data: ReplyData,
655 request: &Request,
656 #[cfg(feature = "websocket")] ws_upgrade: Option<(hyper::upgrade::OnUpgrade, HeaderValue)>,
657 ) -> HyperResponse<ResponseBody> {
658 // A handler that returned `ws::upgrade(...)`: complete the handshake
659 // instead of finalizing a body. `finalize_reply` only lets us reach
660 // here for an `Upgrade` reply when the request *was* a real
661 // handshake (otherwise it rewrote the reply to a 426 error), so
662 // `ws_upgrade` is guaranteed `Some` on this branch.
663 #[cfg(feature = "websocket")]
664 if matches!(data, ReplyData::Upgrade(_)) {
665 let ReplyData::Upgrade(handler) = data else {
666 unreachable!()
667 };
668 let ws_upgrade =
669 ws_upgrade.expect("finalize_reply rewrites Upgrade-without-handshake to 426");
670 return self.complete_ws_upgrade(handler, ws_upgrade).await;
671 }
672 // Compression is the last transform — after response middleware,
673 // before the bytes leave. (Only buffered, compressible bodies above
674 // the threshold are touched.)
675 #[cfg(feature = "compression")]
676 if let Some(c) = &self.compression {
677 data = c.compress_reply(
678 data,
679 request
680 .headers
681 .get("accept-encoding")
682 .and_then(|v| v.to_str().ok()),
683 );
684 }
685 let response = self.finalizer.build_response(data).await;
686 let response = self.with_cors_headers(request, response);
687 #[cfg(feature = "compression")]
688 let response = crate::compression::tag_vary_if_encoded(response);
689 response
690 }
691
692 /// Handles an individual incoming `hyper::Request`.
693 ///
694 /// Wraps [`handle_request_inner`](Self::handle_request_inner) in a
695 /// per-request timeout when one is configured (see
696 /// [`Server::with_request_timeout`]); a timed-out request gets a
697 /// one-shot `504 Gateway Timeout` (no after-chain, since by definition
698 /// something upstream was unresponsive).
699 ///
700 /// Every reply with a `Request` flows through
701 /// [`finalize_reply`](Self::finalize_reply) — handler successes,
702 /// `Outcome::Respond` short-circuits, and *every* error (middleware
703 /// `Err`, body parse failure, 404 / 405 from the router, handler-returned
704 /// `Err`, even the 413 / 400 from the body-cap path). The after-chain,
705 /// compression, and CORS apply uniformly. CORS preflight is the one
706 /// short-circuit that bypasses the pipeline — it's HTTP-protocol traffic
707 /// rather than an application request.
708 async fn handle_request(
709 self: Arc<Self>,
710 req: HyperRequest<Incoming>,
711 ) -> Result<HyperResponse<ResponseBody>, hyper::Error> {
712 let timeout = self.request_timeout;
713 let app = self.clone();
714 let inner = app.handle_request_inner(req);
715 match timeout {
716 None => inner.await,
717 Some(d) => match tokio::time::timeout(d, inner).await {
718 Ok(r) => r,
719 Err(_) => {
720 warn!(timeout = ?d, "request exceeded configured timeout");
721 Ok(self.finalizer.build_error(WebError::Timeout).await)
722 }
723 },
724 }
725 }
726
727 /// The actual request pipeline. Split from
728 /// [`handle_request`](Self::handle_request) so the latter can wrap it
729 /// in a timeout when one is configured.
730 ///
731 /// **Lifecycle order:**
732 ///
733 /// 1. capture WS upgrade (if request looks like a handshake)
734 /// 2. build the `Request` skeleton (no body yet)
735 /// 3. CORS preflight short-circuit (uses headers only)
736 /// 4. match controller — 404 short-circuits *without* buffering the
737 /// body (efficiency win on adversarial bad-path requests); then stamp
738 /// the matched controller's rate-limit class onto the request, so a
739 /// `before` middleware (which only gets `&Request`) can read it
740 /// 5. buffer the body, capped per the resolved policy (today: server-
741 /// wide; soon, per-controller / per-route)
742 /// 6. middleware `before`
743 /// 7. `to_params` (Content-Type-driven body parse)
744 /// 8. dispatch via the already-matched controller
745 /// 9. middleware `after` + finalize
746 ///
747 /// The "route before buffer" order is what lets the body cap depend on
748 /// the matched route — without it, the framework would have to commit
749 /// to a single cap before knowing where the request is headed.
750 async fn handle_request_inner(
751 self: Arc<Self>,
752 #[allow(unused_mut)] mut req: HyperRequest<Incoming>,
753 ) -> Result<HyperResponse<ResponseBody>, hyper::Error> {
754 let request_span = span!(Level::INFO, "request");
755 async move {
756 // 1. Capture the WS upgrade handshake (if any) before
757 // `from_hyper_parts` consumes the request: the `OnUpgrade`
758 // future and the derived `Sec-WebSocket-Accept`. (See
759 // `websocket` module docs for why this happens up front.)
760 #[cfg(feature = "websocket")]
761 let ws_upgrade: Option<(hyper::upgrade::OnUpgrade, HeaderValue)> =
762 if websocket::is_upgrade_request(req.method(), req.headers()) {
763 websocket::accept_key(req.headers())
764 .map(|accept| (hyper::upgrade::on(&mut req), accept))
765 } else {
766 None
767 };
768
769 // 2. Build the skeleton (method / path / query / headers); the
770 // body stream is held aside for step 5.
771 let (mut request, body_stream) = Request::from_hyper_parts(req);
772
773 // 3. CORS preflight: synthesize the 204 ourselves before any
774 // application-layer work. Preflights are HTTP-protocol
775 // traffic; neither `before` nor `after` middleware runs on
776 // them (see `CLAUDE.md` principle 1).
777 if let Some(cors) = &self.cors
778 && CorsLayer::is_preflight(&request.method, &request.headers)
779 {
780 let mut resp = self.finalizer.build_response(ReplyData::Empty).await;
781 cors.apply(&request.headers, resp.headers_mut(), true);
782 return Ok(resp);
783 }
784
785 // 4. Match controller. A path that hits nothing is 404 *before*
786 // body buffering — a 10 MiB POST to a non-existent URL no
787 // longer wastes 10 MiB of memory.
788 let route_match = match self.router.match_controller(&request.path_parts) {
789 Some(rm) => rm,
790 None => {
791 return Ok(self
792 .finalize_error(
793 WebError::NotFound,
794 &request,
795 #[cfg(feature = "websocket")]
796 ws_upgrade,
797 )
798 .await);
799 }
800 };
801
802 // 4b. Stamp the matched controller's rate-limit class onto the
803 // request (the skeleton predates routing, so it was `None`).
804 // This is the one piece of routing context a `before`
805 // middleware can't otherwise see — it gets `&mut Request`,
806 // not the matched controller. An application rate-limit
807 // middleware reads `request.rate_limit_class` and applies its
808 // own per-class policy; the framework owns the label and the
809 // `429` response, not the limiter. (Set before the body
810 // buffer so it survives `collect_body` and is present for the
811 // whole pipeline, including the after-chain on error replies.)
812 request.rate_limit_class = route_match.controller.actus_rate_limit();
813
814 // 5. Resolve the effective body cap: the matched controller's
815 // `#[controller(max_body_bytes = …)]` if it set one, otherwise
816 // the server-wide `with_max_body_bytes` cap (default 2 MiB). A
817 // future Phase 2 adds per-route overrides at the top of this
818 // fall-through.
819 //
820 // The error path returns the same skeleton so the after-chain
821 // still has a `Request`.
822 let effective_cap = route_match
823 .controller
824 .actus_max_body_bytes()
825 .unwrap_or(self.max_body_bytes);
826 request = match request
827 .collect_body(
828 body_stream,
829 effective_cap,
830 self.max_inflight_body_bytes.as_ref(),
831 )
832 .await
833 {
834 Ok(r) => r,
835 Err((request, e)) => {
836 warn!("rejecting request before parse: {}", e);
837 return Ok(self
838 .finalize_error(
839 e,
840 &request,
841 #[cfg(feature = "websocket")]
842 ws_upgrade,
843 )
844 .await);
845 }
846 };
847
848 // 6. Middleware `before` chain.
849 let pre_data: Option<ReplyData> =
850 match self.middleware_chain.process_request(&mut request).await {
851 Ok(Outcome::Continue) => None,
852 Ok(Outcome::Respond(data)) => Some(data),
853 Err(e) => {
854 return Ok(self
855 .finalize_error(
856 e,
857 &request,
858 #[cfg(feature = "websocket")]
859 ws_upgrade,
860 )
861 .await);
862 }
863 };
864
865 // A `before` hook short-circuited with a reply — skip routing
866 // and the handler, but still run the after-chain.
867 if let Some(data) = pre_data {
868 return Ok(self
869 .finalize_reply(
870 data,
871 &request,
872 #[cfg(feature = "websocket")]
873 ws_upgrade,
874 )
875 .await);
876 }
877
878 // 7. Body parse (JSON / form / opaque, per Content-Type).
879 // Malformed body → 400 through the after-chain.
880 let params = match request.to_params() {
881 Ok(p) => p,
882 Err(e) => {
883 return Ok(self
884 .finalize_error(
885 e,
886 &request,
887 #[cfg(feature = "websocket")]
888 ws_upgrade,
889 )
890 .await);
891 }
892 };
893
894 // 8. Dispatch via the matched controller. 405 (verb mismatch
895 // inside the controller) and handler-returned errors both
896 // come back through here.
897 match route_match
898 .controller
899 .actus_dispatch(&route_match.action, params)
900 .await
901 {
902 Ok(data) => Ok(self
903 .finalize_reply(
904 data,
905 &request,
906 #[cfg(feature = "websocket")]
907 ws_upgrade,
908 )
909 .await),
910 Err(e) => Ok(self
911 .finalize_error(
912 e,
913 &request,
914 #[cfg(feature = "websocket")]
915 ws_upgrade,
916 )
917 .await),
918 }
919 }
920 .instrument(request_span)
921 .await
922 }
923}
924
925/// Default shutdown trigger: resolves on SIGTERM, SIGINT (Unix), or Ctrl-C
926/// (Windows). This is what [`Server::run`] uses; for tests or embedding,
927/// see [`Server::run_with_shutdown`].
928async fn default_shutdown_signal() {
929 #[cfg(unix)]
930 {
931 use tokio::signal::unix::{SignalKind, signal};
932 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
933 let mut sigint = signal(SignalKind::interrupt()).expect("install SIGINT handler");
934 tokio::select! {
935 _ = sigterm.recv() => info!("Received SIGTERM"),
936 _ = sigint.recv() => info!("Received SIGINT"),
937 }
938 }
939 #[cfg(not(unix))]
940 {
941 tokio::signal::ctrl_c()
942 .await
943 .expect("install Ctrl-C handler");
944 info!("Received Ctrl-C");
945 }
946}