Skip to main content

churust_lab/
body_limit.rs

1//! A body limit written in the type rather than configured beside it.
2
3use async_trait::async_trait;
4use churust_core::{Call, FromCall, Result, RouteBodyLimit};
5
6/// Wraps a body extractor with a size limit expressed as a const generic.
7///
8/// `BodyLimit<Json<Avatar>, { 1 << 20 }>` is a one-megabyte JSON body. The
9/// limit is part of the handler's signature, so it is visible at the call site
10/// and cannot drift away from the route it protects.
11///
12/// # Why this is worth trying
13///
14/// Churust's [`RouteBuilder::max_body_bytes`](churust_core::RouteBuilder::max_body_bytes)
15/// already attaches a limit to a route, and it is attached to the builder, so
16/// it cannot be registered against the wrong thing. The failure mode this
17/// avoids belongs to a *different* design: config resolved at runtime by type
18/// lookup, which silently falls back to a default when registered on the wrong
19/// scope. A limit that quietly is not applied is worse than no limit, and
20/// putting it in the type makes "not applied" unrepresentable.
21///
22/// What is still unsettled — and why this lives in the lab — is whether the
23/// ergonomics earn their place next to the builder method, which reads better
24/// and does not push a const generic through every signature.
25///
26/// # Interaction with the other limits
27///
28/// This only ever *tightens*. The server-wide `max_body_bytes` is enforced by
29/// the engine before any extractor runs, and a per-route limit applies
30/// underneath that. Whichever is smallest wins.
31///
32/// ```
33/// use churust_core::{Churust, TestClient};
34/// use churust_lab::BodyLimit;
35/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
36/// let app = Churust::server()
37///     .routing(|r| {
38///         r.post("/note", |BodyLimit(text): BodyLimit<String, 16>| async move {
39///             format!("{} bytes", text.len())
40///         });
41///     })
42///     .build();
43///
44/// let client = TestClient::new(app);
45/// assert_eq!(client.post("/note").body("short").send().await.text(), "5 bytes");
46/// assert_eq!(
47///     client.post("/note").body("far too long to fit in sixteen").send().await.status(),
48///     http::StatusCode::PAYLOAD_TOO_LARGE
49/// );
50/// # });
51/// ```
52#[derive(Debug, Clone)]
53pub struct BodyLimit<T, const LIMIT: usize>(
54    /// The extracted value.
55    pub T,
56);
57
58#[async_trait]
59impl<T, const LIMIT: usize> FromCall for BodyLimit<T, LIMIT>
60where
61    T: FromCall,
62{
63    async fn from_call(mut call: Call) -> Result<Self> {
64        // Seed the same per-route limit the builder sets, rather than measuring
65        // here. Measuring would mean collecting the body first — the exact
66        // allocation the limit exists to prevent — and it would bypass the
67        // streaming path, where the cap must trip at the byte that crosses the
68        // line rather than after everything has been read.
69        //
70        // Tighten only: an outer limit already in force stays in force.
71        let effective = match call.get::<RouteBodyLimit>() {
72            Some(RouteBodyLimit(outer)) => outer.min(LIMIT),
73            None => LIMIT,
74        };
75        call.insert(RouteBodyLimit(effective));
76        T::from_call(call).await.map(BodyLimit)
77    }
78}