Skip to main content

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