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