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