Skip to main content

churust_core/
app.rs

1//! Application assembly: the [`Churust`] entry point, the [`AppBuilder`] DSL,
2//! the immutable [`App`], the [`Plugin`] trait, and the resolved
3//! [`ServerConfig`].
4
5use crate::call::Call;
6use crate::pipeline::{Endpoint, Middleware, Next, Phase};
7use crate::response::Response;
8use crate::router::{Match, RouteBuilder, Router};
9use crate::state::StateMap;
10use bytes::Bytes;
11use futures_util::FutureExt;
12use http::header::ALLOW;
13use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
14use std::collections::VecDeque;
15use std::sync::Arc;
16
17/// A reusable bundle of behavior that installs itself into an [`AppBuilder`]
18/// at build time — Churust's analogue of Ktor's `install(Plugin)`.
19///
20/// A plugin typically registers one or more [`Middleware`] (and may add state)
21/// inside [`install`](Plugin::install). Pass a plugin to
22/// [`AppBuilder::install`]; the builder boxes it and calls `install`.
23///
24/// ```
25/// use churust_core::{App, AppBuilder, Churust, Call, Middleware, Next, Plugin, Response, TestClient};
26/// use async_trait::async_trait;
27/// use std::sync::Arc;
28/// use http::{header::HeaderName, HeaderValue};
29///
30/// struct Mark;
31/// #[async_trait]
32/// impl Middleware for Mark {
33///     async fn handle(&self, call: Call, next: Next) -> Response {
34///         let mut res = next.run(call).await;
35///         res.headers.insert(HeaderName::from_static("x-plugin"), HeaderValue::from_static("on"));
36///         res
37///     }
38/// }
39///
40/// struct MarkPlugin;
41/// impl Plugin for MarkPlugin {
42///     fn install(self: Box<Self>, app: &mut AppBuilder) {
43///         app.add_middleware(Arc::new(Mark));
44///     }
45/// }
46/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
47/// let app = Churust::server()
48///     .install(MarkPlugin)
49///     .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
50///     .build();
51/// let res = TestClient::new(app).get("/").send().await;
52/// assert_eq!(res.header("x-plugin"), Some("on"));
53/// # });
54/// ```
55pub trait Plugin {
56    /// Install this plugin into the builder (register middleware, state, etc.).
57    /// Consumes the boxed plugin.
58    fn install(self: Box<Self>, app: &mut AppBuilder);
59}
60
61/// The server configuration resolved at build time and carried by an [`App`].
62///
63/// Populated from defaults, an optional [`Config`](crate::Config), and the
64/// builder's DSL setters. Read it back from a built app with
65/// [`App::config`].
66#[derive(Clone, Debug)]
67pub struct ServerConfig {
68    /// Bind address host.
69    pub host: String,
70    /// Bind port.
71    pub port: u16,
72    /// Maximum accepted request body size in bytes; larger bodies get `413`.
73    pub max_body_bytes: usize,
74    /// Per-request timeout in milliseconds; `0` disables the timeout.
75    pub request_timeout_ms: u64,
76    /// Deadline for a client to finish sending its header block; `0` disables.
77    pub header_read_timeout_ms: u64,
78    /// Maximum number of request headers accepted.
79    pub max_headers: usize,
80    /// Maximum path segments before a request is rejected with `414`.
81    pub max_path_segments: usize,
82    /// Maximum WebSocket frame size in bytes (`ws` feature).
83    pub ws_max_frame_bytes: usize,
84    /// Maximum reassembled WebSocket message size in bytes (`ws` feature).
85    pub ws_max_message_bytes: usize,
86    /// Idle keep-alive in milliseconds; `0` disables connection reuse.
87    /// Idle means no request in flight, so a slow handler is never cut off.
88    pub keep_alive_ms: u64,
89    /// Listen backlog.
90    pub backlog: u32,
91    /// Graceful-shutdown grace period in milliseconds; `0` waits forever.
92    pub shutdown_timeout_ms: u64,
93    /// What to do with a non-canonical path spelling.
94    pub path_policy: crate::path::PathPolicy,
95    /// Maximum size of a received HTTP/2 header block, in bytes. The h2
96    /// counterpart of `max_headers`, which configures HTTP/1 only.
97    pub h2_max_header_list_size: u32,
98    /// Maximum concurrent HTTP/2 streams per connection; `0` removes the limit.
99    pub h2_max_concurrent_streams: u32,
100    /// WebSocket idle bound in milliseconds; `0` disables it (`ws` feature).
101    pub ws_idle_timeout_ms: u64,
102    /// Maximum simultaneously served connections; `0` means unlimited.
103    pub max_connections: usize,
104    /// Maximum TLS handshakes in progress at once; `0` means unlimited.
105    pub max_tls_handshakes: usize,
106    /// TLS handshake deadline in milliseconds; `0` disables the bound.
107    pub tls_handshake_timeout_ms: u64,
108    /// TLS settings, or `None` for plaintext HTTP.
109    pub tls: Option<crate::config::TlsSection>,
110}
111
112impl Default for ServerConfig {
113    fn default() -> Self {
114        Self {
115            host: "127.0.0.1".into(),
116            port: 8080,
117            max_body_bytes: 1 << 20,
118            request_timeout_ms: 30_000,
119            header_read_timeout_ms: 10_000,
120            max_headers: 100,
121            max_path_segments: 64,
122            ws_max_frame_bytes: 1 << 20,
123            ws_max_message_bytes: 4 << 20,
124            keep_alive_ms: 75_000,
125            backlog: 1024,
126            shutdown_timeout_ms: 30_000,
127            path_policy: crate::path::PathPolicy::Strict,
128            h2_max_header_list_size: 16 << 10,
129            h2_max_concurrent_streams: 200,
130            ws_idle_timeout_ms: 300_000,
131            max_connections: 25_000,
132            max_tls_handshakes: 256,
133            tls_handshake_timeout_ms: 10_000,
134            tls: None,
135        }
136    }
137}
138
139/// The fluent builder for an application, returned by [`Churust::server`].
140///
141/// Chain DSL methods to configure the server ([`host`](AppBuilder::host),
142/// [`port`](AppBuilder::port), [`tls`](AppBuilder::tls), ...), register shared
143/// [`state`](AppBuilder::state), [`install`](AppBuilder::install) plugins, and
144/// define routes with [`routing`](AppBuilder::routing). Finish with
145/// [`build`](AppBuilder::build) to get an [`App`], or
146/// [`start`](AppBuilder::start) to build and serve in one step. DSL setters take
147/// precedence over a [`with_config`](AppBuilder::with_config) applied earlier.
148///
149/// ```
150/// use churust_core::{Churust, Call, TestClient};
151/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
152/// let app = Churust::server()
153///     .port(3000)
154///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
155///     .build();
156/// assert_eq!(app.config().port, 3000);
157/// let res = TestClient::new(app).get("/").send().await;
158/// assert_eq!(res.text(), "hi");
159/// # });
160/// ```
161pub struct AppBuilder {
162    router: Router,
163    middleware: Vec<(Phase, Arc<dyn Middleware>)>,
164    config: ServerConfig,
165    state: StateMap,
166    /// `None` means the application opted out. Defaults to the conservative
167    /// set, so an app that never mentions security headers still sends them.
168    security: Option<crate::security::SecurityHeaders>,
169    /// Extra `host:port` strings from [`AppBuilder::bind`].
170    extra_binds: Vec<String>,
171    /// Optional renderer for error statuses. See [`AppBuilder::on_error`].
172    on_error: Option<ErrorRenderer>,
173}
174
175/// Renders an error status into a response. Returning `None` keeps the default.
176type ErrorRenderer = Arc<dyn Fn(StatusCode, &Call) -> Option<Response> + Send + Sync>;
177
178impl AppBuilder {
179    fn new() -> Self {
180        Self {
181            router: Router::new(),
182            middleware: Vec::new(),
183            config: ServerConfig::default(),
184            state: StateMap::default(),
185            security: Some(crate::security::SecurityHeaders::default()),
186            on_error: None,
187            extra_binds: Vec::new(),
188        }
189    }
190
191    /// Set the bind host (default `"127.0.0.1"`). Returns `self` for chaining.
192    pub fn host(mut self, host: impl Into<String>) -> Self {
193        self.config.host = host.into();
194        self
195    }
196    /// Set the bind port (default `8080`). Returns `self` for chaining.
197    pub fn port(mut self, port: u16) -> Self {
198        self.config.port = port;
199        self
200    }
201    /// Set the maximum accepted request body size in bytes (default `1 MiB`).
202    /// Larger bodies are rejected with `413 Payload Too Large`. Returns `self`
203    /// for chaining.
204    pub fn max_body_bytes(mut self, n: usize) -> Self {
205        self.config.max_body_bytes = n;
206        self
207    }
208
209    /// Apply a fully-resolved [`Config`](crate::Config), overwriting the current
210    /// server settings.
211    ///
212    /// This is lowest precedence: DSL setters called *after* it (e.g. another
213    /// [`port`](AppBuilder::port)) win. Use it to seed the builder from a config
214    /// file/env, then override specific fields in code. Returns `self` for
215    /// chaining.
216    pub fn with_config(mut self, cfg: crate::config::Config) -> Self {
217        self.config.host = cfg.server.host;
218        self.config.port = cfg.server.port;
219        self.config.max_body_bytes = cfg.server.max_body_bytes;
220        self.config.request_timeout_ms = cfg.server.request_timeout_ms;
221        self.config.header_read_timeout_ms = cfg.server.header_read_timeout_ms;
222        self.config.max_headers = cfg.server.max_headers;
223        self.config.max_path_segments = cfg.server.max_path_segments;
224        self.config.ws_max_frame_bytes = cfg.server.ws_max_frame_bytes;
225        self.config.ws_max_message_bytes = cfg.server.ws_max_message_bytes;
226        self.config.keep_alive_ms = cfg.server.keep_alive_ms;
227        self.config.backlog = cfg.server.backlog;
228        self.config.shutdown_timeout_ms = cfg.server.shutdown_timeout_ms;
229        self.config.path_policy = cfg.server.path_policy;
230        self.config.h2_max_header_list_size = cfg.server.h2_max_header_list_size;
231        self.config.h2_max_concurrent_streams = cfg.server.h2_max_concurrent_streams;
232        self.config.ws_idle_timeout_ms = cfg.server.ws_idle_timeout_ms;
233        self.config.max_connections = cfg.server.max_connections;
234        self.config.max_tls_handshakes = cfg.server.max_tls_handshakes;
235        self.config.tls_handshake_timeout_ms = cfg.server.tls_handshake_timeout_ms;
236        self.config.tls = cfg.tls;
237        self
238    }
239
240    /// Set the per-request timeout in milliseconds (default `30000`). A value of
241    /// `0` disables the timeout. Requests exceeding it get `408 Request Timeout`.
242    /// Returns `self` for chaining.
243    pub fn request_timeout_ms(mut self, ms: u64) -> Self {
244        self.config.request_timeout_ms = ms;
245        self
246    }
247
248    /// Set how long a client may take to send its complete header block
249    /// (default `10000` ms; `0` disables).
250    ///
251    /// This is the slow-loris defence. The per-request timeout cannot cover it,
252    /// because there is no request until the headers arrive.
253    pub fn header_read_timeout_ms(mut self, ms: u64) -> Self {
254        self.config.header_read_timeout_ms = ms;
255        self
256    }
257
258    /// Set the maximum number of request headers accepted (default `100`).
259    pub fn max_headers(mut self, n: usize) -> Self {
260        self.config.max_headers = n;
261        self
262    }
263
264    /// Set how long an idle connection is kept for reuse (default `75000` ms;
265    /// `0` disables keep-alive and closes after each response).
266    ///
267    /// A connection with a request in flight is not idle, however long the
268    /// handler takes. Lower this when connection count matters more than
269    /// round-trip latency; raise it for chatty clients on slow links.
270    ///
271    /// Over HTTP/3 this becomes the QUIC idle timeout, so a lowered bound
272    /// applies there too. The one exception is `0`: a QUIC connection
273    /// multiplexes streams and cannot be closed after a single response, so
274    /// there is nothing for "answer and close" to mean and the HTTP/3 listener
275    /// keeps the default `75000` ms bound rather than never expiring.
276    ///
277    /// `0` reaches HTTP/2 as well, where hyper has no reuse switch to turn off:
278    /// the connection is closed once a response has been written and nothing
279    /// else is in flight. A connection that has not answered anything yet is
280    /// left to `header_read_timeout_ms`, which is what bounds a peer that has
281    /// not made a request.
282    pub fn keep_alive_ms(mut self, ms: u64) -> Self {
283        self.config.keep_alive_ms = ms;
284        self
285    }
286
287    /// Set the listen backlog (default `1024`).
288    pub fn backlog(mut self, n: u32) -> Self {
289        self.config.backlog = n;
290        self
291    }
292
293    /// Set how long graceful shutdown waits for in-flight requests (default
294    /// `30000` ms; `0` waits indefinitely).
295    ///
296    /// Unbounded waiting means one slow request can delay shutdown forever,
297    /// which in a container means being killed rather than exiting cleanly.
298    pub fn shutdown_timeout_ms(mut self, ms: u64) -> Self {
299        self.config.shutdown_timeout_ms = ms;
300        self
301    }
302
303    /// Set what happens to a non-canonical path spelling (default
304    /// [`PathPolicy::Strict`](crate::PathPolicy::Strict)).
305    ///
306    /// `//a`, `/a//b` and `/a/` are aliases of `/a`. Serving them silently
307    /// gives one resource several URLs, which makes prefix-based checks
308    /// bypassable and cache identity ambiguous.
309    pub fn path_policy(mut self, policy: crate::path::PathPolicy) -> Self {
310        self.config.path_policy = policy;
311        self
312    }
313
314    /// Set the maximum size of a received HTTP/2 header block in bytes
315    /// (default `16384`).
316    ///
317    /// `max_headers` configures HTTP/1 only — it counts headers, and HTTP/2 has
318    /// no equivalent count, only an encoded size. Set both if you serve both.
319    pub fn h2_max_header_list_size(mut self, n: u32) -> Self {
320        self.config.h2_max_header_list_size = n;
321        self
322    }
323
324    /// Set the maximum concurrent HTTP/2 streams per connection (default
325    /// `200`; `0` removes the limit).
326    ///
327    /// An h2 connection multiplexes many requests, so this is what stops one
328    /// connection from becoming an unbounded amount of concurrent work.
329    pub fn h2_max_concurrent_streams(mut self, n: u32) -> Self {
330        self.config.h2_max_concurrent_streams = n;
331        self
332    }
333
334    /// Set how long an upgraded WebSocket may sit idle before it is closed
335    /// (default `300000` ms; `0` disables the bound).
336    ///
337    /// An upgraded socket holds a connection permit for its whole life, and no
338    /// HTTP-level timeout survives the upgrade — so without this a peer that
339    /// completes the handshake and then goes silent holds a permit until the
340    /// process restarts. Idle means no frame in either direction.
341    pub fn ws_idle_timeout_ms(mut self, ms: u64) -> Self {
342        self.config.ws_idle_timeout_ms = ms;
343        self
344    }
345
346    /// Set the maximum number of simultaneously served connections (default
347    /// `25000`; `0` means unlimited).
348    ///
349    /// The backlog bounds what the kernel queues before the accept loop reaches
350    /// it; this bounds what the process serves at once. Excess connections wait
351    /// for a slot rather than being accepted, so the pressure shows up as
352    /// latency instead of as an out-of-memory or out-of-descriptors death.
353    pub fn max_connections(mut self, n: usize) -> Self {
354        self.config.max_connections = n;
355        self
356    }
357
358    /// Set the maximum number of TLS handshakes in progress at once (default
359    /// `256`; `0` means unlimited).
360    ///
361    /// Deliberately far below `max_connections`: a handshake is asymmetric
362    /// work, cheap for the client to request and expensive for the server to
363    /// perform, so it gets its own tighter bound.
364    ///
365    /// Applies to HTTP/3 as well, whose QUIC handshake is a TLS 1.3 handshake
366    /// and asymmetric in the same way.
367    pub fn max_tls_handshakes(mut self, n: usize) -> Self {
368        self.config.max_tls_handshakes = n;
369        self
370    }
371
372    /// Set how long a TLS handshake may take before the connection is dropped
373    /// (default `10000` ms; `0` disables the bound).
374    ///
375    /// `header_read_timeout_ms` cannot cover this: until the handshake
376    /// finishes there is no HTTP layer to time out. Without it, a client that
377    /// completes the TCP handshake and then dribbles bytes holds a connection
378    /// open indefinitely.
379    ///
380    /// Applies to HTTP/3 too, and bounds the wait for the
381    /// [`max_tls_handshakes`](Self::max_tls_handshakes) budget as well as the
382    /// handshake itself — a peer queued for that budget is already holding a
383    /// connection permit, so timing only the handshake would leave the wait
384    /// unbounded.
385    pub fn tls_handshake_timeout_ms(mut self, ms: u64) -> Self {
386        self.config.tls_handshake_timeout_ms = ms;
387        self
388    }
389
390    /// Set the maximum number of path segments accepted (default `64`).
391    /// Longer paths are rejected with `414 URI Too Long`.
392    pub fn max_path_segments(mut self, n: usize) -> Self {
393        self.config.max_path_segments = n;
394        self
395    }
396
397    /// Enable TLS, reading the certificate chain from `cert_path` and the
398    /// private key from `key_path` (PEM). The files are loaded when the server
399    /// starts; this only records the paths. Requires the `tls` feature to have
400    /// any effect at serve time. Returns `self` for chaining.
401    pub fn tls(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
402        self.config.tls = Some(crate::config::TlsSection {
403            cert: cert_path.into(),
404            key: key_path.into(),
405        });
406        self
407    }
408
409    /// Register a shared application-state value of type `T`, retrieved later
410    /// via the [`State`](crate::State) extractor or
411    /// [`Call::state`](crate::Call::state). One value is held per type;
412    /// registering another `T` replaces it. Returns `self` for chaining.
413    ///
414    /// ```
415    /// use churust_core::{Churust, State, TestClient};
416    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
417    /// #[derive(Clone)]
418    /// struct Db { name: &'static str }
419    /// let app = Churust::server()
420    ///     .state(Db { name: "postgres" })
421    ///     .routing(|r| { r.get("/", |db: State<Db>| async move { db.name }); })
422    ///     .build();
423    /// assert_eq!(TestClient::new(app).get("/").send().await.text(), "postgres");
424    /// # });
425    /// ```
426    pub fn state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
427        self.insert_state(value);
428        self
429    }
430
431    /// Advertise HTTP/3 on `port` to clients arriving over TCP.
432    ///
433    /// Adds `Alt-Svc: h3=":<port>"; ma=86400` to every response, which is how a
434    /// browser learns that h3 exists at all: it reaches a new origin over TCP,
435    /// reads this header, and uses QUIC for subsequent requests. Serving h3
436    /// without advertising it means almost nothing will ever use it.
437    ///
438    /// Available with or without the `http3` feature, because the process that
439    /// terminates h3 is often a proxy in front rather than this server. Setting
440    /// it when nothing is listening on that UDP port costs a client one failed
441    /// QUIC attempt before it falls back, so only set it when something is.
442    ///
443    /// ```
444    /// use churust_core::{Call, Churust, TestClient};
445    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
446    /// let app = Churust::server()
447    ///     .advertise_http3(8443)
448    ///     .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
449    ///     .build();
450    /// let res = TestClient::new(app).get("/").send().await;
451    /// assert_eq!(res.header("alt-svc"), Some("h3=\":8443\"; ma=86400"));
452    /// # });
453    /// ```
454    pub fn advertise_http3(mut self, port: u16) -> Self {
455        self.add_middleware_in(Phase::Setup, Arc::new(AltSvc(port)));
456        self
457    }
458
459    /// Register application state through a mutable borrow.
460    ///
461    /// The counterpart to [`state`](Self::state) for callers that hold
462    /// `&mut AppBuilder` rather than owning it, which is every [`Plugin`]: a
463    /// plugin that wants to publish something for its own extractor to find has
464    /// no other way to reach the state map. Same pairing as
465    /// [`add_middleware`](Self::add_middleware) and
466    /// [`install_middleware`](Self::install_middleware).
467    pub fn insert_state<T: Send + Sync + 'static>(&mut self, value: T) {
468        self.state.insert(value);
469    }
470
471    /// Install a [`Plugin`], letting it register middleware/state. Consumes the
472    /// plugin value. Returns `self` for chaining.
473    pub fn install<P: Plugin + 'static>(mut self, plugin: P) -> Self {
474        Box::new(plugin).install(&mut self);
475        self
476    }
477
478    /// Register a [`Middleware`] in a specific [`Phase`]. Plugins use this to
479    /// place their middleware precisely; the builder sorts all middleware by
480    /// phase (stably) at [`build`](AppBuilder::build) time.
481    pub fn add_middleware_in(&mut self, phase: Phase, mw: Arc<dyn Middleware>) {
482        self.middleware.push((phase, mw));
483    }
484
485    /// Register a [`Middleware`] in the default [`Phase::Plugins`] phase — the
486    /// common case for application middleware.
487    pub fn add_middleware(&mut self, mw: Arc<dyn Middleware>) {
488        self.add_middleware_in(Phase::Plugins, mw);
489    }
490
491    /// Define routes via the [`RouteBuilder`] DSL. The closure receives a
492    /// mutable builder on which to register handlers and nested scopes. Returns
493    /// `self` for chaining.
494    ///
495    /// ```
496    /// use churust_core::{Churust, Call, TestClient};
497    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
498    /// let app = Churust::server()
499    ///     .routing(|r| {
500    ///         r.get("/", |_c: Call| async { "home" });
501    ///         r.post("/echo", |mut c: Call| async move { c.receive_text().await.unwrap_or_default() });
502    ///     })
503    ///     .build();
504    /// assert_eq!(TestClient::new(app).get("/").send().await.text(), "home");
505    /// # });
506    /// ```
507    pub fn routing(mut self, f: impl FnOnce(&mut RouteBuilder)) -> Self {
508        let mut b = RouteBuilder::new(&mut self.router);
509        f(&mut b);
510        self
511    }
512
513    /// Every route registered so far, as `(method, pattern)`.
514    ///
515    /// Called between two [`routing`](Self::routing) blocks, this is the
516    /// inventory an API description is generated from: describe what is already
517    /// registered, then register the route that serves the description.
518    ///
519    /// ```
520    /// use churust_core::{Call, Churust};
521    /// use http::Method;
522    ///
523    /// let builder = Churust::server().routing(|r| {
524    ///     r.get("/users/{id}", |_c: Call| async { "" });
525    /// });
526    /// assert_eq!(builder.routes(), &[(Method::GET, "/users/{id}".to_string())]);
527    /// ```
528    pub fn routes(&self) -> &[(Method, String)] {
529        self.router.routes()
530    }
531
532    /// Build the app and serve it until Ctrl-C — a shorthand for
533    /// `self.build().start().await`. Binds a socket, so it does not return until
534    /// shutdown.
535    ///
536    /// ```no_run
537    /// use churust_core::{Churust, Call};
538    /// # async fn run() -> std::io::Result<()> {
539    /// Churust::server()
540    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
541    ///     .start()
542    ///     .await
543    /// # }
544    /// ```
545    pub async fn start(self) -> std::io::Result<()> {
546        // Genuinely nothing but `build().start()`. The extra-address handling
547        // that used to live here is now in `App`, where every entry point can
548        // reach it; keeping a second copy here is how the two got to disagree.
549        self.build().start().await
550    }
551
552    /// Render error responses yourself — v1 design §5.3's "StatusPages-lite".
553    ///
554    /// The closure runs for any response the pipeline produces with a `4xx` or
555    /// `5xx` status, whether it came from a handler returning `Err`, a `404`
556    /// for an unmatched path, or a `405`. Return `Some(response)` to replace
557    /// it, or `None` to keep the default rendering — so a hook can take over
558    /// just the statuses it cares about.
559    ///
560    /// It does **not** run for a request the server refused to admit: an
561    /// oversized `Content-Length`, a message framed both by `Transfer-Encoding`
562    /// and `Content-Length`, or a deadline that expired before the pipeline
563    /// returned anything. Those are answered by the transport as `413`, `400`
564    /// and `408` in plain text, and deliberately so — running the pipeline
565    /// would mean inventing a `Call` for a request that was never accepted, and
566    /// every middleware with a side effect (a rate-limit counter, an audit
567    /// entry, a session touch) would then record a request the server declined
568    /// to dispatch. Security headers *are* applied to them, because those are
569    /// added by the transport on the way out rather than by the pipeline.
570    ///
571    /// It receives the status rather than the `Error` because by the time a
572    /// response exists the error has already been rendered; this is the same
573    /// shape as Ktor's `StatusPages`, which Churust's pipeline is modelled on.
574    ///
575    /// ```
576    /// use churust_core::{Call, Churust, Response, TestClient};
577    /// use http::StatusCode;
578    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
579    /// let app = Churust::server()
580    ///     .on_error(|status, call| {
581    ///         (status == StatusCode::NOT_FOUND)
582    ///             .then(|| Response::text(format!("no {} here", call.path())).with_status(status))
583    ///     })
584    ///     .routing(|r| { r.get("/", |_c: Call| async { "home" }); })
585    ///     .build();
586    ///
587    /// let res = TestClient::new(app).get("/missing").send().await;
588    /// assert_eq!(res.text(), "no /missing here");
589    /// # });
590    /// ```
591    pub fn on_error<F>(mut self, f: F) -> Self
592    where
593        F: Fn(StatusCode, &Call) -> Option<Response> + Send + Sync + 'static,
594    {
595        self.on_error = Some(Arc::new(f));
596        self
597    }
598
599    /// Install a single [`Middleware`] app-wide, in the `Plugins` phase.
600    ///
601    /// The chainable counterpart to [`add_middleware`](AppBuilder::add_middleware),
602    /// which exists for plugins holding a `&mut AppBuilder`. For middleware that
603    /// should apply to only part of the route tree, use
604    /// [`RouteBuilder::intercept`](crate::RouteBuilder::intercept) instead.
605    pub fn install_middleware<M: Middleware>(mut self, mw: M) -> Self {
606        self.middleware.push((Phase::Plugins, Arc::new(mw)));
607        self
608    }
609
610    /// Replace the default [`SecurityHeaders`](crate::SecurityHeaders) set.
611    ///
612    /// ```
613    /// use churust_core::{Churust, SecurityHeaders};
614    /// # fn build() {
615    /// Churust::server()
616    ///     .security_headers(SecurityHeaders::new().frame_options(Some("SAMEORIGIN")));
617    /// # }
618    /// ```
619    pub fn security_headers(mut self, headers: crate::security::SecurityHeaders) -> Self {
620        self.security = Some(headers);
621        self
622    }
623
624    /// Send no security headers at all.
625    ///
626    /// Reach for this only when something in front of the server already adds
627    /// them; the defaults exist because most applications never get around to
628    /// setting them by hand.
629    pub fn without_security_headers(mut self) -> Self {
630        self.security = None;
631        self
632    }
633
634    /// Bind an additional address.
635    ///
636    /// Call it more than once to listen on several — IPv4 and IPv6, or a public
637    /// port alongside an admin one. The configured host and port are always
638    /// bound; this adds to them.
639    ///
640    /// The addresses survive [`build`](Self::build), so they are honoured by
641    /// [`App::start`] and [`App::start_with_shutdown`] just as much as by
642    /// [`start`](Self::start). The two exceptions are the entry points that are
643    /// handed a socket instead of choosing one — [`App::start_on`] takes an
644    /// already-bound listener and [`start_unix`](Self::start_unix) serves a
645    /// filesystem path — and neither binds the configured `host:port` either,
646    /// so there is nothing for this to add to.
647    ///
648    /// ```no_run
649    /// use churust_core::{Call, Churust};
650    /// # async fn run() -> std::io::Result<()> {
651    /// Churust::server()
652    ///     .host("127.0.0.1")
653    ///     .port(8080)
654    ///     .bind("[::1]:8080")
655    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
656    ///     .start()
657    ///     .await
658    /// # }
659    /// ```
660    pub fn bind(mut self, addr: impl Into<String>) -> Self {
661        self.extra_binds.push(addr.into());
662        self
663    }
664
665    /// Serve on a Unix domain socket until Ctrl-C.
666    ///
667    /// See [`engine::serve_unix`](crate::engine::serve_unix). Unix only.
668    #[cfg(unix)]
669    pub async fn start_unix(self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
670        self.build().start_unix(path).await
671    }
672
673    /// Finish building into an immutable, cheaply-cloneable [`App`].
674    ///
675    /// Installed middleware is sorted by [`Phase`] (stably, preserving install
676    /// order within a phase) and the configuration is frozen.
677    pub fn build(self) -> App {
678        let mut mw = self.middleware;
679
680        // Setup phase, so it wraps everything and post-processes every response
681        // — including 404s and errors, which are just as reachable as handler
682        // output. Pushed before the sort, which is stable, so an application's
683        // own Setup middleware installed earlier still runs outside this.
684        if let Some(render) = self.on_error {
685            // Monitoring rather than Setup: security headers live in Setup and
686            // must wrap this, so a replaced error page is protected too.
687            mw.push((
688                Phase::Monitoring,
689                Arc::new(ErrorPages { render }) as Arc<dyn Middleware>,
690            ));
691        }
692
693        // Kept as well as installed. The middleware covers everything the
694        // pipeline produces, but a transport also answers on its own — a body
695        // refused on its declared length, a deadline that expired before the
696        // pipeline returned anything — and those responses have to be given the
697        // same set by the transport itself. It cannot ask the middleware,
698        // because by then it is an opaque `Arc<dyn Middleware>` in a list.
699        let security = self.security.clone();
700        if let Some(headers) = self.security {
701            let tls_enabled = self.config.tls.is_some();
702            mw.push((
703                Phase::Setup,
704                Arc::new(headers.into_middleware(tls_enabled)) as Arc<dyn Middleware>,
705            ));
706        }
707
708        mw.sort_by_key(|(phase, _)| *phase); // stable: install order preserved within a phase
709        let middleware: Vec<Arc<dyn Middleware>> = mw.into_iter().map(|(_, m)| m).collect();
710        App {
711            inner: Arc::new(AppInner {
712                router: self.router,
713                middleware,
714                config: self.config,
715                state: Arc::new(self.state),
716                extra_binds: self.extra_binds,
717                security,
718            }),
719        }
720    }
721}
722
723struct AppInner {
724    router: Router,
725    middleware: Vec<Arc<dyn Middleware>>,
726    config: ServerConfig,
727    state: Arc<StateMap>,
728    /// Carried through from [`AppBuilder::bind`]. `build` used to drop these,
729    /// which made `bind` a no-op for everybody who built an [`App`] and then
730    /// served it — the ordinary shape once a custom shutdown signal is in play.
731    /// The extra addresses are serve-time state rather than request-time state,
732    /// but so are `host` and `port`, which already ride along in `config`.
733    extra_binds: Vec<String>,
734    /// The same set the pipeline middleware was built from, for the responses
735    /// a transport writes without going through the pipeline. `None` when the
736    /// application called `without_security_headers`, so the opt-out reaches
737    /// those responses too.
738    security: Option<crate::security::SecurityHeaders>,
739}
740
741/// An assembled, immutable, cheaply-cloneable application.
742///
743/// Produced by [`AppBuilder::build`]. Internally reference-counted, so cloning
744/// is cheap and clones share the same router, middleware, state, and config.
745/// Serve it with [`start`](App::start) (or
746/// [`start_with_shutdown`](App::start_with_shutdown)), drive a single request
747/// in-process with [`process`](App::process), or hand it to a
748/// [`TestClient`](crate::TestClient) for testing.
749///
750/// ```
751/// use churust_core::{Churust, Call, TestClient};
752/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
753/// let app = Churust::server()
754///     .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
755///     .build();
756/// let clone = app.clone(); // cheap; shares the same inner app
757/// let res = TestClient::new(clone).get("/").send().await;
758/// assert_eq!(res.status().as_u16(), 200);
759/// # });
760/// ```
761#[derive(Clone)]
762pub struct App {
763    inner: Arc<AppInner>,
764}
765
766impl App {
767    /// The resolved [`ServerConfig`] this app was built with.
768    pub fn config(&self) -> &ServerConfig {
769        &self.inner.config
770    }
771
772    /// Decorate a response with the configured
773    /// [`SecurityHeaders`](crate::SecurityHeaders).
774    ///
775    /// For the transports to call on the way out, on every response — not only
776    /// on the ones they synthesised. Sorting those two apart would mean a new
777    /// refusal added later is bare until someone notices, which is exactly how
778    /// the pre-dispatch `413` and the h3 `413` came to be bare. Calling it on a
779    /// response the pipeline already decorated costs a handful of header
780    /// lookups and changes nothing, since a header already present is kept.
781    ///
782    /// `over_tls` is the transport asserting that this response is encrypted
783    /// regardless of what the builder was told — true for HTTP/3, which has no
784    /// plaintext mode. Over TCP the builder's own certificate is the only
785    /// evidence available, so the flag is false there and
786    /// `config.tls.is_some()` decides.
787    pub(crate) fn apply_security_headers(&self, headers: &mut HeaderMap, over_tls: bool) {
788        if let Some(security) = &self.inner.security {
789            security.apply_to(headers, over_tls || self.inner.config.tls.is_some());
790        }
791    }
792
793    /// The single request entry point: run one request through the full
794    /// pipeline (middleware then routed handler) and return the [`Response`].
795    ///
796    /// Both the hyper engine and the [`TestClient`](crate::TestClient) call
797    /// this. It is panic-isolated — a panicking handler is caught and turned
798    /// into `500 Internal Server Error` rather than crashing the task.
799    ///
800    /// ```
801    /// use churust_core::{Churust, Call};
802    /// use http::{HeaderMap, Method};
803    /// use bytes::Bytes;
804    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
805    /// let app = Churust::server()
806    ///     .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
807    ///     .build();
808    /// let res = app.process(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new()).await;
809    /// assert_eq!(res.status.as_u16(), 200);
810    /// # });
811    /// ```
812    /// THE single request entry point used by the engine and `TestClient`.
813    /// Panic-isolated: a panicking handler yields 500.
814    pub async fn process(
815        &self,
816        method: Method,
817        uri: Uri,
818        headers: HeaderMap,
819        body: Bytes,
820    ) -> Response {
821        self.process_with_extensions(method, uri, headers, body, http::Extensions::new())
822            .await
823    }
824
825    /// Like [`App::process`], but seeds the `Call` with pre-built extensions
826    /// (e.g. a captured WebSocket upgrade handle). Advanced/engine use.
827    pub async fn process_with_extensions(
828        &self,
829        method: Method,
830        uri: Uri,
831        headers: HeaderMap,
832        body: Bytes,
833        extensions: http::Extensions,
834    ) -> Response {
835        self.process_call(Call::new(method, uri, headers, body), extensions)
836            .await
837    }
838
839    /// Run the pipeline over an already-built [`Call`]. Engine use: this is how
840    /// a streaming request body reaches a handler without being buffered first.
841    pub(crate) async fn process_call(&self, call: Call, extensions: http::Extensions) -> Response {
842        let app = self.clone();
843        let fut = async move {
844            let mut call = call;
845            call.seed_extensions(extensions);
846            call.set_state(app.inner.state.clone());
847            app.run_pipeline(call).await
848        };
849        match std::panic::AssertUnwindSafe(fut).catch_unwind().await {
850            Ok(res) => res,
851            Err(_) => Response::text("Internal Server Error")
852                .with_status(StatusCode::INTERNAL_SERVER_ERROR),
853        }
854    }
855
856    /// Every TCP address this app should listen on: the configured `host:port`
857    /// first, then whatever [`AppBuilder::bind`] added.
858    ///
859    /// One list built in one place, so the address-based entry points cannot
860    /// drift apart on which addresses count — that drift is exactly how `bind`
861    /// came to be honoured by `AppBuilder::start` and silently ignored by every
862    /// other way of serving the same application.
863    fn bind_addrs(&self) -> std::io::Result<Vec<std::net::SocketAddr>> {
864        std::iter::once(format!(
865            "{}:{}",
866            self.inner.config.host, self.inner.config.port
867        ))
868        .chain(self.inner.extra_binds.iter().cloned())
869        .map(|a| {
870            a.parse::<std::net::SocketAddr>()
871                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
872        })
873        .collect()
874    }
875
876    /// Bind the configured address — plus anything [`AppBuilder::bind`] added —
877    /// and serve until Ctrl-C (SIGINT), then drain in-flight connections
878    /// gracefully. Does not return until shutdown.
879    ///
880    /// # Errors
881    ///
882    /// Returns an [`std::io::Error`] if any configured address is invalid or a
883    /// socket cannot be bound (e.g. the port is in use).
884    ///
885    /// ```no_run
886    /// use churust_core::{Churust, Call};
887    /// # async fn run() -> std::io::Result<()> {
888    /// let app = Churust::server()
889    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
890    ///     .build();
891    /// app.start().await
892    /// # }
893    /// ```
894    pub async fn start(self) -> std::io::Result<()> {
895        self.start_with_shutdown(async {
896            let _ = tokio::signal::ctrl_c().await;
897        })
898        .await
899    }
900
901    /// Bind and serve until the provided `shutdown` future resolves, then drain
902    /// gracefully. Use this to wire a custom shutdown signal (e.g. in tests, or
903    /// to combine SIGTERM with SIGINT).
904    ///
905    /// Serves every address [`AppBuilder::bind`] registered as well as the
906    /// configured `host:port`; a bind failure on any one of them aborts the
907    /// whole start rather than leaving a half-up server.
908    ///
909    /// # Errors
910    ///
911    /// Returns an [`std::io::Error`] if any configured address is invalid or a
912    /// socket cannot be bound.
913    ///
914    /// ```no_run
915    /// use churust_core::{Churust, Call};
916    /// # async fn run() -> std::io::Result<()> {
917    /// let app = Churust::server()
918    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
919    ///     .build();
920    /// let (tx, rx) = tokio::sync::oneshot::channel::<()>();
921    /// // ... later: tx.send(()) to trigger shutdown ...
922    /// app.start_with_shutdown(async { let _ = rx.await; }).await
923    /// # }
924    /// ```
925    pub async fn start_with_shutdown<F>(self, shutdown: F) -> std::io::Result<()>
926    where
927        F: std::future::Future<Output = ()> + Send + 'static,
928    {
929        let mut addrs = self.bind_addrs()?;
930        // The single-address case keeps going through `serve`, which serves the
931        // accept loop on this task. `serve_many` has to spawn one task per
932        // listener and fan a broadcast out to them, so routing the common case
933        // through it would change how a panic or a join failure surfaces for
934        // every existing caller, to buy nothing.
935        if addrs.len() == 1 {
936            return crate::engine::serve(self, addrs.remove(0), shutdown).await;
937        }
938        crate::engine::serve_many(self, addrs, shutdown).await
939    }
940
941    /// Serve on an already-bound listener until `shutdown` resolves.
942    ///
943    /// The counterpart to [`start_with_shutdown`](App::start_with_shutdown) for
944    /// callers that must know the port before the server runs — a supervisor
945    /// passing a socket in, or a test that needs the address.
946    ///
947    /// It also closes a race the address-based entry points cannot: finding a
948    /// free port by binding, dropping, and letting the server bind again leaves
949    /// a window for something else to claim it. Handing over the listener
950    /// removes the window.
951    ///
952    /// ```no_run
953    /// use churust_core::{Churust, Call};
954    /// # async fn f() -> std::io::Result<()> {
955    /// let app = Churust::server()
956    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
957    ///     .build();
958    /// let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
959    /// println!("listening on {}", listener.local_addr()?);
960    /// app.start_on(listener, async { let _ = tokio::signal::ctrl_c().await; }).await
961    /// # }
962    /// ```
963    pub async fn start_on<F>(
964        self,
965        listener: tokio::net::TcpListener,
966        shutdown: F,
967    ) -> std::io::Result<()>
968    where
969        F: std::future::Future<Output = ()> + Send + 'static,
970    {
971        crate::engine::serve_on(self, listener, shutdown).await
972    }
973
974    /// Serve on a Unix domain socket until Ctrl-C, then drain.
975    #[cfg(unix)]
976    pub async fn start_unix(self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
977        crate::engine::serve_unix(self, path, async {
978            let _ = tokio::signal::ctrl_c().await;
979        })
980        .await
981    }
982
983    async fn run_pipeline(&self, call: Call) -> Response {
984        let inner = self.inner.clone();
985        let endpoint: Endpoint = Arc::new(move |mut call: Call| {
986            let inner = inner.clone();
987            Box::pin(async move {
988                let path = call.path().to_string();
989                let method = call.method().clone();
990
991                // RFC 9110 §9.3.7: `OPTIONS *` asks about the server as a
992                // whole, not about a resource. Routing it as a path meant a
993                // `404`, which says the server does not exist — the one answer
994                // that is definitely wrong to a capability probe.
995                if method == Method::OPTIONS && path == "*" {
996                    let value = crate::router::allow_header_value(inner.router.all_methods())
997                        .unwrap_or_else(|| Method::OPTIONS.as_str().to_string());
998                    return Response::new(StatusCode::NO_CONTENT).with_header(
999                        ALLOW,
1000                        HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
1001                    );
1002                }
1003
1004                // Resolve the path spelling once, here rather than in the
1005                // router, so there is a single decision point for it.
1006                //
1007                // "Here" is the endpoint, which is the *innermost* layer: the
1008                // middleware chain has already run on the way in, so middleware
1009                // observes the raw spelling and only the router and the
1010                // handlers are downstream of the decision. That is deliberate,
1011                // not an oversight. A refusal or a redirect is an ordinary
1012                // response that has to travel back out through the chain to be
1013                // finished — security headers live in `Phase::Setup` and
1014                // `on_error` in `Phase::Monitoring`, and a `404` is exactly as
1015                // reachable as handler output, so a decision taken outside the
1016                // chain would ship an alias refusal with neither.
1017                //
1018                // Nothing is bypassable by it under `Strict` or `Redirect`:
1019                // whatever a prefix-keyed middleware concludes from the raw
1020                // spelling, the endpoint replaces the response regardless, so
1021                // no handler runs. `Collapse` does keep the hazard, because it
1022                // serves the alias and does not rewrite the URI — which is what
1023                // `PathPolicy::Collapse` documents itself as, and why it is a
1024                // migration step rather than a supported posture.
1025                if let Some(canonical) = crate::path::canonical_path(&path) {
1026                    match inner.config.path_policy {
1027                        crate::path::PathPolicy::Strict => {
1028                            return Response::text("Not Found").with_status(StatusCode::NOT_FOUND);
1029                        }
1030                        crate::path::PathPolicy::Redirect => {
1031                            // Carry the query across: dropping it would change
1032                            // the request while claiming to be the same one.
1033                            let target = match call.uri().query() {
1034                                Some(q) if !q.is_empty() => format!("{canonical}?{q}"),
1035                                _ => canonical,
1036                            };
1037                            // 308 rather than 301: a 301 lets a client retry a
1038                            // POST as a GET, which silently discards the body.
1039                            return Response::new(StatusCode::PERMANENT_REDIRECT).with_header(
1040                                http::header::LOCATION,
1041                                HeaderValue::from_str(&target)
1042                                    .unwrap_or(HeaderValue::from_static("/")),
1043                            );
1044                        }
1045                        // Fall through and let the router collapse them.
1046                        crate::path::PathPolicy::Collapse => {}
1047                    }
1048                }
1049
1050                // Refuse an over-deep path before the router walks it. `walk`
1051                // recurses once per segment with backtracking, so depth is a
1052                // stack-depth question rather than merely a cost one.
1053                if inner.config.max_path_segments > 0
1054                    && path.split('/').filter(|s| !s.is_empty()).count()
1055                        > inner.config.max_path_segments
1056                {
1057                    return Response::text("URI Too Long").with_status(StatusCode::URI_TOO_LONG);
1058                }
1059
1060                let mut lookup = inner.router.route(&method, &path, &call);
1061
1062                // RFC 9110 §9.3.2: HEAD must be available wherever GET is.
1063                // Only synthesized when no HEAD route was registered, so an
1064                // explicit HEAD handler always wins.
1065                let mut synthesized_head = false;
1066                if method == Method::HEAD && !matches!(lookup, Match::Found { .. }) {
1067                    if let m @ Match::Found { .. } = inner.router.route(&Method::GET, &path, &call)
1068                    {
1069                        lookup = m;
1070                        synthesized_head = true;
1071                    }
1072                }
1073
1074                // Automatic OPTIONS, only where no handler claimed the request.
1075                // CORS runs in the Plugins phase and short-circuits preflight
1076                // before this endpoint is reached, so an installed Cors keeps
1077                // priority over this.
1078                if method == Method::OPTIONS && !matches!(lookup, Match::Found { .. }) {
1079                    if let Some(value) =
1080                        crate::router::allow_header_value(inner.router.methods_for(&path))
1081                    {
1082                        return Response::new(StatusCode::NO_CONTENT).with_header(
1083                            ALLOW,
1084                            HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
1085                        );
1086                    }
1087                }
1088
1089                match lookup {
1090                    Match::Found { handler, params } => {
1091                        call.set_params(params);
1092                        let res = handler.handle(call).await;
1093                        if synthesized_head {
1094                            strip_body(res)
1095                        } else {
1096                            res
1097                        }
1098                    }
1099                    // Same generator as the `OPTIONS` arm above: one resource,
1100                    // one answer. `None` means nothing is registered here at
1101                    // all, which is a `404` — a `405` with an empty `Allow`
1102                    // would tell the client to retry with nothing.
1103                    Match::MethodNotAllowed { allow } => {
1104                        match crate::router::allow_header_value(allow) {
1105                            Some(value) => Response::new(StatusCode::METHOD_NOT_ALLOWED)
1106                                .with_header(
1107                                    ALLOW,
1108                                    HeaderValue::from_str(&value)
1109                                        .unwrap_or(HeaderValue::from_static("")),
1110                                ),
1111                            None => Response::text("Not Found").with_status(StatusCode::NOT_FOUND),
1112                        }
1113                    }
1114                    Match::NotFound => {
1115                        Response::text("Not Found").with_status(StatusCode::NOT_FOUND)
1116                    }
1117                    // A malformed escape or non-UTF-8 bytes in the path. The
1118                    // request is unintelligible rather than merely unmatched,
1119                    // so it is a 400 and not a 404.
1120                    Match::BadPath => {
1121                        Response::text("Bad Request").with_status(StatusCode::BAD_REQUEST)
1122                    }
1123                }
1124            }) as _
1125        });
1126
1127        let chain: VecDeque<Arc<dyn Middleware>> = self.inner.middleware.iter().cloned().collect();
1128        Next::new(chain, endpoint).run(call).await
1129    }
1130}
1131
1132/// Adds the `Alt-Svc` header that points TCP clients at HTTP/3.
1133///
1134/// In [`Phase::Setup`], the outermost phase, so it is present on error
1135/// responses too. A client that only ever gets a `404` from an origin should
1136/// still learn that the origin speaks h3.
1137struct AltSvc(u16);
1138
1139#[async_trait::async_trait]
1140impl Middleware for AltSvc {
1141    async fn handle(&self, call: Call, next: Next) -> Response {
1142        let mut res = next.run(call).await;
1143        // `ma` is how long the client may remember this, in seconds. A day is
1144        // the common choice: long enough to matter, short enough that turning
1145        // h3 off does not strand clients for a week.
1146        if let Ok(value) = http::HeaderValue::from_str(&format!("h3=\":{}\"; ma=86400", self.0)) {
1147            res.headers
1148                .insert(http::header::HeaderName::from_static("alt-svc"), value);
1149        }
1150        res
1151    }
1152}
1153
1154/// Runs the application's [`on_error`](AppBuilder::on_error) renderer over any
1155/// error response the pipeline produces. A connection-level refusal is answered
1156/// before there is a pipeline to run — see the note on `on_error`.
1157struct ErrorPages {
1158    render: ErrorRenderer,
1159}
1160
1161#[async_trait::async_trait]
1162impl Middleware for ErrorPages {
1163    async fn handle(&self, call: Call, next: Next) -> Response {
1164        // The renderer needs the request, and `next.run` consumes the call, so
1165        // keep the parts it can ask about.
1166        let snapshot = call.snapshot_for_error();
1167        let res = next.run(call).await;
1168        if res.status.is_client_error() || res.status.is_server_error() {
1169            if let Some(mut replacement) = (self.render)(res.status, &snapshot) {
1170                // Carry over headers the renderer did not set. Some of them are
1171                // not decoration: RFC 9110 §15.5.6 requires `Allow` on a `405`,
1172                // §15.5.2 requires `WWW-Authenticate` on a `401`, and a `416`
1173                // carries `Content-Range`. Replacing the whole response dropped
1174                // them, so installing `on_error` silently made those responses
1175                // non-conforming. The renderer still wins wherever it sets a
1176                // header itself.
1177                for name in res.headers.keys() {
1178                    if replacement.headers.contains_key(name) {
1179                        continue;
1180                    }
1181                    // Never carry a header that describes the body that was
1182                    // just replaced. `Content-Length` in particular is framed
1183                    // on by hyper, so grafting the original's length onto a
1184                    // different body truncates or hangs the response; with the
1185                    // header absent hyper measures the real body.
1186                    if name == http::header::CONTENT_LENGTH
1187                        || name == http::header::CONTENT_TYPE
1188                        || name == http::header::CONTENT_ENCODING
1189                    {
1190                        continue;
1191                    }
1192                    // Every value, not just the first: `HeaderMap::iter` yields
1193                    // one item per value, so a `contains_key` guard inside that
1194                    // loop kept only the first `Set-Cookie` of several.
1195                    for value in res.headers.get_all(name) {
1196                        replacement.headers.append(name.clone(), value.clone());
1197                    }
1198                }
1199                return replacement;
1200            }
1201        }
1202        res
1203    }
1204}
1205
1206/// Drop a response body for a synthesized `HEAD` reply, keeping status and
1207/// headers.
1208///
1209/// RFC 9110 §9.3.2 says a `HEAD` response should carry the same header fields a
1210/// `GET` would, so a buffered body's length is preserved as `Content-Length`
1211/// before the bytes are discarded — clients do use `HEAD` to size a resource.
1212///
1213/// A streamed body has no known length and is dropped rather than drained:
1214/// draining it would do exactly the work the client declined to ask for. The
1215/// same section permits omitting fields that are only determined while
1216/// generating the content, which is precisely this case.
1217fn strip_body(mut res: Response) -> Response {
1218    if let Some(bytes) = res.body.as_bytes() {
1219        if let Ok(value) = HeaderValue::from_str(&bytes.len().to_string()) {
1220            res.headers.insert(http::header::CONTENT_LENGTH, value);
1221        }
1222    }
1223    res.body = crate::body::Body::empty();
1224    res
1225}
1226
1227/// The framework entry point — a zero-sized namespace for starting an
1228/// [`AppBuilder`].
1229///
1230/// Begin with [`Churust::server`] for an empty builder, or
1231/// [`Churust::from_config`] to seed it from `churust.toml` plus `CHURUST_*`
1232/// environment variables.
1233///
1234/// ```
1235/// use churust_core::{Churust, Call, TestClient};
1236/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1237/// let app = Churust::server()
1238///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
1239///     .build();
1240/// assert_eq!(TestClient::new(app).get("/").send().await.text(), "hi");
1241/// # });
1242/// ```
1243pub struct Churust;
1244
1245impl Churust {
1246    /// Start a fresh [`AppBuilder`] with default configuration.
1247    pub fn server() -> AppBuilder {
1248        AppBuilder::new()
1249    }
1250
1251    /// Start an [`AppBuilder`] pre-loaded from `churust.toml` and `CHURUST_*`
1252    /// environment variables (via [`Config::load_default`](crate::Config::load_default)).
1253    /// Chain DSL setters afterward to override individual fields.
1254    ///
1255    /// ```no_run
1256    /// use churust_core::{Churust, Call};
1257    /// // Loads churust.toml + env, then overrides the port in code.
1258    /// let app = Churust::from_config()
1259    ///     .port(3000)
1260    ///     .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
1261    ///     .build();
1262    /// let _ = app;
1263    /// ```
1264    pub fn from_config() -> AppBuilder {
1265        AppBuilder::new().with_config(crate::config::Config::load_default())
1266    }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271    use super::*;
1272    use crate::pipeline::Next;
1273    use async_trait::async_trait;
1274
1275    #[tokio::test]
1276    async fn middleware_runs_in_phase_order() {
1277        use crate::pipeline::{Next, Phase};
1278        use async_trait::async_trait;
1279        use std::sync::{Arc, Mutex};
1280
1281        #[derive(Clone)]
1282        struct Recorder {
1283            log: Arc<Mutex<Vec<&'static str>>>,
1284            tag: &'static str,
1285        }
1286        #[async_trait]
1287        impl Middleware for Recorder {
1288            async fn handle(&self, call: Call, next: Next) -> Response {
1289                self.log.lock().unwrap().push(self.tag);
1290                next.run(call).await
1291            }
1292        }
1293
1294        let log = Arc::new(Mutex::new(Vec::new()));
1295        let mut builder = Churust::server();
1296        // Install OUT of phase order; expect execution IN phase order.
1297        builder.add_middleware_in(
1298            Phase::Fallback,
1299            Arc::new(Recorder {
1300                log: log.clone(),
1301                tag: "fallback",
1302            }),
1303        );
1304        builder.add_middleware_in(
1305            Phase::Setup,
1306            Arc::new(Recorder {
1307                log: log.clone(),
1308                tag: "setup",
1309            }),
1310        );
1311        builder.add_middleware_in(
1312            Phase::Monitoring,
1313            Arc::new(Recorder {
1314                log: log.clone(),
1315                tag: "monitoring",
1316            }),
1317        );
1318        let app = builder
1319            .routing(|r| {
1320                r.get("/", |_c: Call| async { "ok" });
1321            })
1322            .build();
1323        let _ = get(&app, "/").await;
1324        assert_eq!(
1325            *log.lock().unwrap(),
1326            vec!["setup", "monitoring", "fallback"]
1327        );
1328    }
1329
1330    fn app() -> App {
1331        Churust::server()
1332            .routing(|r| {
1333                r.get("/", |_c: Call| async { "home" });
1334                r.get("/boom", |_c: Call| async {
1335                    panic!("handler exploded");
1336                    #[allow(unreachable_code)]
1337                    ""
1338                });
1339            })
1340            .build()
1341    }
1342
1343    async fn get(app: &App, path: &str) -> Response {
1344        app.process(
1345            Method::GET,
1346            path.parse::<Uri>().unwrap(),
1347            HeaderMap::new(),
1348            Bytes::new(),
1349        )
1350        .await
1351    }
1352
1353    #[tokio::test]
1354    async fn routes_to_handler() {
1355        let res = get(&app(), "/").await;
1356        assert_eq!(res.status, StatusCode::OK);
1357        assert_eq!(res.body, Bytes::from("home"));
1358    }
1359
1360    #[tokio::test]
1361    async fn unknown_path_is_404() {
1362        let res = get(&app(), "/missing").await;
1363        assert_eq!(res.status, StatusCode::NOT_FOUND);
1364    }
1365
1366    #[tokio::test]
1367    async fn panicking_handler_yields_500_not_crash() {
1368        let res = get(&app(), "/boom").await;
1369        assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
1370    }
1371
1372    #[tokio::test]
1373    async fn process_with_extensions_seeds_call() {
1374        #[derive(Clone)]
1375        struct Marker(u32);
1376        let app = Churust::server()
1377            .routing(|r| {
1378                r.get("/", |c: Call| async move {
1379                    format!("{}", c.get::<Marker>().map(|m| m.0).unwrap_or(0))
1380                });
1381            })
1382            .build();
1383        let mut ext = http::Extensions::new();
1384        ext.insert(Marker(7));
1385        let res = app
1386            .process_with_extensions(
1387                Method::GET,
1388                "/".parse().unwrap(),
1389                HeaderMap::new(),
1390                Bytes::new(),
1391                ext,
1392            )
1393            .await;
1394        assert_eq!(res.body, Bytes::from("7"));
1395    }
1396
1397    #[tokio::test]
1398    async fn state_extractor_end_to_end() {
1399        use crate::extract::State;
1400        #[derive(Clone)]
1401        struct Counter(u32);
1402
1403        let app = Churust::server()
1404            .state(Counter(5))
1405            .routing(|r| {
1406                r.get(
1407                    "/n",
1408                    |s: State<Counter>| async move { format!("n={}", s.0 .0) },
1409                );
1410            })
1411            .build();
1412        let res = get(&app, "/n").await;
1413        assert_eq!(res.body, Bytes::from("n=5"));
1414    }
1415
1416    #[tokio::test]
1417    async fn middleware_installed_via_plugin_runs() {
1418        struct MarkPlugin;
1419        struct Mark;
1420        #[async_trait]
1421        impl Middleware for Mark {
1422            async fn handle(&self, call: Call, next: Next) -> Response {
1423                let mut res = next.run(call).await;
1424                res.headers.insert(
1425                    http::header::HeaderName::from_static("x-plugin"),
1426                    HeaderValue::from_static("on"),
1427                );
1428                res
1429            }
1430        }
1431        impl Plugin for MarkPlugin {
1432            fn install(self: Box<Self>, app: &mut AppBuilder) {
1433                app.add_middleware(Arc::new(Mark));
1434            }
1435        }
1436
1437        let app = Churust::server()
1438            .install(MarkPlugin)
1439            .routing(|r| {
1440                r.get("/", |_c: Call| async { "ok" });
1441            })
1442            .build();
1443        let res = get(&app, "/").await;
1444        assert_eq!(res.headers.get("x-plugin").unwrap(), "on");
1445    }
1446}