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}