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}