Skip to main content

jerrycan_core/
extract.rs

1//! Request context and extractors (spec §4.1). Everything a handler needs is
2//! visible in its signature; each parameter implements [`FromRequest`].
3
4use crate::dep::DepResolver;
5use crate::error::{Error, Result};
6use crate::response::Json;
7use bytes::Bytes;
8use serde::de::DeserializeOwned;
9use std::future::Future;
10
11/// A live, incrementally-arriving request body: hyper's stream, pre-wrapped in
12/// the route's cumulative `Limited` cap and the per-frame read deadline.
13/// Unsync (hyper's body is not Sync); the lane lives inside one dispatch task.
14pub(crate) type StreamLane =
15    http_body_util::combinators::UnsyncBoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
16
17/// How the request body reaches the context. Buffered routes collect the body
18/// upfront (the v2.0b two-phase read); `.stream_body()` routes hand the live
19/// hyper stream straight through as a [`BodyLane::Stream`].
20pub(crate) enum BodyLane {
21    Buffered(Bytes),
22    /// `None` after a streaming consumer (Multipart, Task 7) took ownership.
23    Stream(Option<StreamLane>),
24}
25
26/// The connection's remote socket address, threaded from the accept loop onto
27/// `parts.extensions` so it survives into the handler. A newtype so the typemap
28/// lookup is unambiguous. `None` for synthetic requests (tasks, some tests).
29#[derive(Clone, Copy, Debug)]
30pub struct ClientAddr(pub std::net::SocketAddr);
31
32/// The mutable view of one in-flight request. Handlers receive extractors,
33/// not this type; middleware and the DI resolver work through it.
34pub struct RequestCtx {
35    pub(crate) parts: http::request::Parts,
36    pub(crate) body: BodyLane,
37    /// Path parameters captured by the router, in route order.
38    pub(crate) params: Vec<(String, String)>,
39    pub(crate) deps: DepResolver,
40    /// True only for a [`TaskContext`](crate::dep::TaskContext): resolution runs
41    /// outside an HTTP request, so HTTP-coupled extractors reject with JC1003.
42    pub(crate) is_task: bool,
43}
44
45impl RequestCtx {
46    /// Buffered-lane constructor: the body is already fully collected. The
47    /// convenience path used by the buffered dispatch route and every test
48    /// helper that hands over pre-read bytes.
49    pub(crate) fn new(parts: http::request::Parts, body: Bytes, deps: DepResolver) -> Self {
50        Self::with_lane(parts, BodyLane::Buffered(body), deps)
51    }
52
53    /// Lane-taking constructor: the streaming dispatch route hands the live
54    /// hyper stream lane straight through without buffering it upfront.
55    pub(crate) fn with_lane(
56        parts: http::request::Parts,
57        body: BodyLane,
58        deps: DepResolver,
59    ) -> Self {
60        Self {
61            parts,
62            body,
63            params: Vec::new(),
64            deps,
65            is_task: false,
66        }
67    }
68
69    /// The complete request body. Buffered lane: a cheap clone. Stream lane:
70    /// drains the stream (the route's `Limited` cap and per-frame deadline are
71    /// inside it) and CACHES the bytes, so repeated extractors keep working.
72    pub(crate) async fn drain_body(&mut self) -> Result<Bytes> {
73        match &mut self.body {
74            BodyLane::Buffered(bytes) => Ok(bytes.clone()),
75            BodyLane::Stream(slot) => {
76                // A `None` slot means a streaming consumer (Multipart, Task 7) took the
77                // lane and left it empty; a later drain on the same request lands here.
78                // This 500 is the intended post-Multipart contract, not dead code.
79                let stream = slot
80                    .take()
81                    .ok_or_else(|| Error::internal("request body was already consumed"))?;
82                use http_body_util::BodyExt;
83                let collected = stream.collect().await.map_err(map_stream_error)?;
84                let bytes = collected.to_bytes();
85                self.body = BodyLane::Buffered(bytes.clone());
86                Ok(bytes)
87            }
88        }
89    }
90
91    pub fn method(&self) -> &http::Method {
92        &self.parts.method
93    }
94    pub fn uri(&self) -> &http::Uri {
95        &self.parts.uri
96    }
97    pub fn headers(&self) -> &http::HeaderMap {
98        &self.parts.headers
99    }
100
101    /// The named path parameter captured by the router, if present. Unlike
102    /// `Path<T>` (which binds the leaf-most param), a guard can address a specific
103    /// mount param by name — e.g. the tenant fk `club_id` under `/clubs/{club_id}`.
104    pub fn param(&self, name: &str) -> Option<&str> {
105        self.params
106            .iter()
107            .find(|(k, _)| k == name)
108            .map(|(_, v)| v.as_str())
109    }
110
111    /// The remote peer's socket address, if the transport provided one. Set by the
112    /// serve loop from `accept()`; absent for task contexts and synthetic requests.
113    /// Rate limiting uses the IP here as its last-resort partition key; treat it as
114    /// the raw TCP peer (a proxy's address behind a load balancer).
115    pub fn peer_addr(&self) -> Option<std::net::SocketAddr> {
116        self.parts.extensions.get::<ClientAddr>().map(|c| c.0)
117    }
118
119    /// Remove a typed extension from the request parts. jerrycan-realtime takes
120    /// hyper's `OnUpgrade` handle this way to run a WebSocket after replying 101.
121    /// Remove-not-get: the handle is single-use and `!Clone`.
122    pub fn take_extension<T: Send + Sync + 'static>(&mut self) -> Option<T> {
123        self.parts.extensions.remove::<T>()
124    }
125}
126
127/// Map a stream-lane read failure onto the stable codes: the route's
128/// cumulative cap → 413, a frame that never arrived → 408 (same code the
129/// buffered read path uses), anything else (client vanished mid-upload) → 400.
130pub(crate) fn map_stream_error(e: Box<dyn std::error::Error + Send + Sync>) -> Error {
131    if e.downcast_ref::<http_body_util::LengthLimitError>()
132        .is_some()
133    {
134        return Error::payload_too_large();
135    }
136    if e.downcast_ref::<crate::serve::RecvTimeout>().is_some() {
137        return Error::new(
138            http::StatusCode::REQUEST_TIMEOUT,
139            "JC0408",
140            "timed out reading the request body",
141        );
142    }
143    Error::bad_request("request body failed mid-read")
144}
145
146/// Types that can be produced from the request. Implemented by all extractors
147/// and by `Dep<T>` (see `dep` module).
148pub trait FromRequest: Sized + Send {
149    fn from_request(ctx: &mut RequestCtx) -> impl Future<Output = Result<Self>> + Send;
150}
151
152/// Typed path parameter: `Path<i64>` binds the LEAF-MOST (last) captured
153/// parameter; use a tuple to address all parameters root→leaf — `Path<(A, B)>` /
154/// `Path<(A, B, C)>` grab two/three `{param}`s in route order. Param types are
155/// the sealed [`PathParam`] set (integers, `String`, `bool`, floats, `char`);
156/// custom newtypes opt in through the [`path_param!`](crate::path_param) macro.
157pub struct Path<T>(pub T);
158
159/// Crate-internal seal for [`PathParam`]. Hidden from docs, but `pub` so the
160/// [`path_param!`](crate::path_param) macro can name it from outside this module
161/// — the trait below stays the real gate, and `path_param!` is its sanctioned door.
162#[doc(hidden)]
163pub mod sealed {
164    pub trait Sealed {}
165}
166
167/// Types extractable from one path segment. The built-in set (integers,
168/// `String`, `bool`, floats, `char`) is sealed; custom param types (id newtypes)
169/// join it through the [`path_param!`](crate::path_param) macro, which is the
170/// only sanctioned way to implement this trait outside the crate.
171pub trait PathParam: sealed::Sealed + Sized + Send {
172    fn parse_param(name: &str, raw: &str) -> Result<Self>;
173}
174
175macro_rules! impl_path_param {
176    ($($t:ty),* $(,)?) => {$(
177        impl sealed::Sealed for $t {}
178        impl PathParam for $t {
179            fn parse_param(name: &str, raw: &str) -> Result<Self> {
180                raw.parse::<$t>().map_err(|e| {
181                    Error::bad_request(format!("invalid path parameter `{name}`: {e}"))
182                })
183            }
184        }
185    )*};
186}
187
188/// Admit a custom newtype as a [`Path`] parameter. The type must implement
189/// [`FromStr`](std::str::FromStr) with a `Display` error; a parse failure maps
190/// to the same `JC0400` invalid-path-parameter error the built-in impls produce.
191///
192/// ```
193/// # use jerrycan_core as jerrycan;
194/// #[derive(Debug)]
195/// struct LeadId(i64);
196/// impl std::str::FromStr for LeadId {
197///     type Err = std::num::ParseIntError;
198///     fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(LeadId(s.parse()?)) }
199/// }
200/// jerrycan::path_param!(LeadId);
201/// ```
202#[macro_export]
203macro_rules! path_param {
204    ($($t:ty),* $(,)?) => {$(
205        impl $crate::extract::sealed::Sealed for $t {}
206        impl $crate::extract::PathParam for $t {
207            fn parse_param(name: &str, raw: &str) -> $crate::Result<Self> {
208                raw.parse::<$t>().map_err(|e| {
209                    $crate::Error::bad_request(format!("invalid path parameter `{name}`: {e}"))
210                })
211            }
212        }
213    )*};
214}
215impl_path_param!(
216    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, char, String,
217);
218
219impl<T: PathParam> FromRequest for Path<T> {
220    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
221        if ctx.is_task {
222            return Err(Error::task_context());
223        }
224        // Binds the leaf-most (last) captured parameter, so a route mounted under
225        // a param-carrying prefix (e.g. `/ws/{ws}` + `/leads/{id}`) addresses its
226        // own `{id}` rather than the mount's `{ws}`. Tuples address all of them.
227        let (name, raw) = ctx
228            .params
229            .last()
230            .ok_or_else(|| Error::internal("route has no path parameters"))?;
231        T::parse_param(name, raw).map(Path)
232    }
233}
234
235impl<A: PathParam, B: PathParam> FromRequest for Path<(A, B)> {
236    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
237        if ctx.is_task {
238            return Err(Error::task_context());
239        }
240        let [a, b] = take_params::<2>(ctx)?;
241        Ok(Path((
242            A::parse_param(&a.0, &a.1)?,
243            B::parse_param(&b.0, &b.1)?,
244        )))
245    }
246}
247
248impl<A: PathParam, B: PathParam, C: PathParam> FromRequest for Path<(A, B, C)> {
249    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
250        if ctx.is_task {
251            return Err(Error::task_context());
252        }
253        let [a, b, c] = take_params::<3>(ctx)?;
254        Ok(Path((
255            A::parse_param(&a.0, &a.1)?,
256            B::parse_param(&b.0, &b.1)?,
257            C::parse_param(&c.0, &c.1)?,
258        )))
259    }
260}
261
262/// First N captured params, cloned in route order. Fewer than N is a routing
263/// bug (the route declared fewer `{params}` than the handler expects) — 500.
264fn take_params<const N: usize>(ctx: &RequestCtx) -> Result<[(String, String); N]> {
265    if ctx.params.len() < N {
266        return Err(Error::internal(format!(
267            "route captures {} path parameter(s) but the handler expects {N}",
268            ctx.params.len()
269        )));
270    }
271    Ok(std::array::from_fn(|i| ctx.params[i].clone()))
272}
273
274/// A by-name view of the request's captured path parameters. Where [`Path<T>`]
275/// binds positionally (the leaf-most segment, or a root→leaf tuple), `PathParams`
276/// reads a SPECIFIC mount param BY NAME — the accessor a DI factory needs, since
277/// a factory resolves each argument through [`FromRequest`] and cannot borrow
278/// `&RequestCtx` to call [`RequestCtx::param`]. The membership-verifying tenancy
279/// guard uses it to read the tenant fk `club_id` under `/clubs/{club_id}` even
280/// when a leaf `{id}` follows (issues #78/#79). Rejects a task context (JC1003),
281/// like every other HTTP-coupled extractor.
282pub struct PathParams(Vec<(String, String)>);
283
284impl PathParams {
285    /// The value of the named path parameter captured by the router, or `None`
286    /// if this route captured no param by that name.
287    pub fn get(&self, name: &str) -> Option<&str> {
288        self.0
289            .iter()
290            .find(|(k, _)| k == name)
291            .map(|(_, v)| v.as_str())
292    }
293}
294
295impl FromRequest for PathParams {
296    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
297        if ctx.is_task {
298            return Err(Error::task_context());
299        }
300        Ok(PathParams(ctx.params.clone()))
301    }
302}
303
304/// Typed query string: `Query<MyParams>` via serde.
305pub struct Query<T>(pub T);
306
307impl<T: DeserializeOwned + Send> FromRequest for Query<T> {
308    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
309        if ctx.is_task {
310            return Err(Error::task_context());
311        }
312        let q = ctx.parts.uri.query().unwrap_or("");
313        serde_urlencoded::from_str::<T>(q)
314            .map(Query)
315            .map_err(|e| Error::bad_request(format!("invalid query string: {e}")))
316    }
317}
318
319impl<T: DeserializeOwned + Send> FromRequest for Json<T> {
320    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
321        if ctx.is_task {
322            return Err(Error::task_context());
323        }
324        let body = ctx.drain_body().await?;
325        serde_json::from_slice::<T>(&body)
326            .map(Json)
327            .map_err(|e| Error::unprocessable(format!("invalid JSON body: {e}")))
328    }
329}
330
331/// Read-only access to request headers in a handler signature.
332pub struct Headers(pub(crate) http::HeaderMap);
333
334impl Headers {
335    /// Header value as a &str, or None if absent or non-ASCII.
336    pub fn get(&self, name: &str) -> Option<&str> {
337        self.0.get(name).and_then(|v| v.to_str().ok())
338    }
339}
340
341impl FromRequest for Headers {
342    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
343        if ctx.is_task {
344            return Err(Error::task_context());
345        }
346        Ok(Headers(ctx.headers().clone()))
347    }
348}
349
350/// The request body as EXACT bytes — the extractor for webhook signature
351/// verification, where the digest must cover the wire bytes, not a re-serialized
352/// value. Works on buffered routes (cheap clone) and `stream_body()` routes
353/// (drains and caches). See the auth docs for the Stripe/Twilio recipes.
354pub struct RawBody(pub Bytes);
355
356impl FromRequest for RawBody {
357    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
358        if ctx.is_task {
359            return Err(Error::task_context());
360        }
361        Ok(RawBody(ctx.drain_body().await?))
362    }
363}
364
365/// Optional extraction: `Some` when the inner extractor succeeds, `None` on
366/// ANY extraction failure. For genuinely optional inputs — the canonical use
367/// is optional auth (`Option<CurrentUser>` on a route that also accepts a
368/// signed URL). Do NOT use it to paper over malformed required input: the
369/// failure reason is discarded by design.
370impl<T: FromRequest> FromRequest for Option<T> {
371    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
372        Ok(T::from_request(ctx).await.ok())
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::dep::DepEnv;
380    use std::sync::Arc;
381
382    fn ctx(uri: &str, body: &str) -> RequestCtx {
383        let req = http::Request::builder()
384            .method(http::Method::GET)
385            .uri(uri)
386            .body(())
387            .unwrap();
388        let (parts, ()) = req.into_parts();
389        RequestCtx::new(
390            parts,
391            Bytes::from(body.to_string()),
392            DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
393        )
394    }
395
396    #[tokio::test]
397    async fn peer_addr_is_none_without_a_socket_and_readable_when_set() {
398        let mut c = ctx("/x", "");
399        assert!(c.peer_addr().is_none());
400        let addr: std::net::SocketAddr = "203.0.113.7:5000".parse().unwrap();
401        c.parts.extensions.insert(crate::extract::ClientAddr(addr));
402        assert_eq!(c.peer_addr(), Some(addr));
403    }
404
405    #[tokio::test]
406    async fn path_extracts_typed_param() {
407        let mut c = ctx("/todos/42", "");
408        c.params.push(("id".into(), "42".into()));
409        let Path(id): Path<i64> = Path::<i64>::from_request(&mut c).await.unwrap();
410        assert_eq!(id, 42);
411    }
412
413    #[test]
414    fn ctx_param_reads_a_named_captured_param() {
415        // WHY: a tenancy/ownership guard must address a SPECIFIC mount param by
416        // name — the tenant fk `club_id` under `/clubs/{club_id}` — not just the
417        // leaf-most param that `Path<T>` binds. `param(name)` is that named read;
418        // it underpins membership verification (issues #78/#79). Absent → None.
419        let mut c = ctx("/x", "");
420        c.params.push(("club_id".into(), "42".into()));
421        c.params.push(("id".into(), "7".into()));
422        assert_eq!(c.param("club_id"), Some("42"));
423        assert_eq!(c.param("id"), Some("7"));
424        assert_eq!(c.param("missing"), None);
425    }
426
427    #[tokio::test]
428    async fn path_params_reads_named_and_rejects_task() {
429        // WHY: a DI factory resolves each arg via `FromRequest` (it cannot borrow
430        // `&RequestCtx`), and `Path<T>` binds only the leaf-most segment — so the
431        // membership-verifying tenancy guard (issues #78/#79) needs a by-NAME read
432        // of a specific mount param (the tenant fk `club_id` under `/clubs/{club_id}`,
433        // even when a leaf `{id}` follows). `PathParams` is that FromRequest accessor.
434        let mut c = ctx("/x", "");
435        c.params.push(("club_id".into(), "42".into()));
436        c.params.push(("id".into(), "7".into()));
437        let p = PathParams::from_request(&mut c).await.unwrap();
438        assert_eq!(p.get("club_id"), Some("42"));
439        assert_eq!(p.get("id"), Some("7"));
440        assert_eq!(p.get("missing"), None);
441
442        // A task context has no request path — reject like every HTTP extractor (JC1003).
443        let mut task = ctx("/x", "");
444        task.is_task = true;
445        let err = PathParams::from_request(&mut task).await.err().unwrap();
446        assert_eq!(err.code(), "JC1003");
447    }
448
449    #[tokio::test]
450    async fn path_with_wrong_type_is_400() {
451        let mut c = ctx("/todos/abc", "");
452        c.params.push(("id".into(), "abc".into()));
453        let err = Path::<i64>::from_request(&mut c).await.err().unwrap();
454        assert_eq!(err.code(), "JC0400");
455    }
456
457    #[tokio::test]
458    async fn path_missing_param_is_500() {
459        // No params captured by the router → internal error (route declared a param
460        // the trie never filled), surfaced as JC0500.
461        let mut c = ctx("/todos", "");
462        let err = Path::<i64>::from_request(&mut c).await.err().unwrap();
463        assert_eq!(err.code(), "JC0500");
464    }
465
466    #[tokio::test]
467    async fn query_deserializes_struct() {
468        #[derive(serde::Deserialize)]
469        struct Page {
470            limit: u32,
471            offset: u32,
472        }
473        let mut c = ctx("/todos?limit=10&offset=20", "");
474        let Query(p): Query<Page> = Query::from_request(&mut c).await.unwrap();
475        assert_eq!((p.limit, p.offset), (10, 20));
476    }
477
478    #[tokio::test]
479    async fn option_extractor_yields_none_on_failure_and_some_on_success() {
480        // WHY: a private bucket's GET must accept EITHER a session OR a signed
481        // URL — the handler needs optional extraction instead of a hard 401
482        // from the extractor. Option<T> is None on ANY extraction failure.
483        #[derive(serde::Deserialize)]
484        struct P {
485            n: i64,
486        }
487        async fn probe(q: Option<Query<P>>) -> Result<Json<Option<i64>>> {
488            Ok(Json(q.map(|Query(p)| p.n)))
489        }
490        let t = crate::App::new()
491            .route("/probe", crate::get(probe))
492            .into_test();
493        assert_eq!(t.get("/probe?n=7").await.text(), "7");
494        // Missing/malformed query → None, not a 400.
495        assert_eq!(t.get("/probe").await.text(), "null");
496        assert_eq!(t.get("/probe?n=not-a-number").await.text(), "null");
497    }
498
499    #[tokio::test]
500    async fn single_path_param_binds_the_leaf_segment() {
501        use crate::prelude::*;
502        async fn show(Path(id): Path<i64>) -> Result<Json<i64>> {
503            Ok(Json(id))
504        }
505        let t = App::new()
506            .mount(
507                "/ws/{ws}",
508                Module::new("leads").route("/leads/{id}", get(show)),
509            )
510            .into_test();
511        assert_eq!(
512            t.get("/ws/7/leads/42").await.json::<i64>(),
513            42,
514            "leaf param, not mount param"
515        );
516    }
517
518    #[tokio::test]
519    async fn tuples_still_read_root_to_leaf() {
520        use crate::prelude::*;
521        async fn pair(Path((ws, id)): Path<(i64, i64)>) -> Result<Json<(i64, i64)>> {
522            Ok(Json((ws, id)))
523        }
524        let t = App::new()
525            .mount(
526                "/ws/{ws}",
527                Module::new("leads").route("/leads/{id}", get(pair)),
528            )
529            .into_test();
530        assert_eq!(t.get("/ws/7/leads/42").await.json::<(i64, i64)>(), (7, 42));
531    }
532
533    #[tokio::test]
534    async fn path_param_macro_admits_custom_newtypes() {
535        use crate::prelude::*;
536        #[derive(Debug)]
537        struct LeadId(i64);
538        impl std::str::FromStr for LeadId {
539            type Err = std::num::ParseIntError;
540            fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
541                Ok(LeadId(s.parse()?))
542            }
543        }
544        crate::path_param!(LeadId);
545        async fn show(Path(id): Path<LeadId>) -> Result<Json<i64>> {
546            Ok(Json(id.0))
547        }
548        let t = App::new().route("/leads/{id}", get(show)).into_test();
549        assert_eq!(t.get("/leads/42").await.json::<i64>(), 42);
550    }
551
552    #[tokio::test]
553    async fn raw_body_yields_exact_bytes_and_coexists_with_headers() {
554        use crate::prelude::*;
555        async fn verify(headers: Headers, body: RawBody) -> Result<Json<(usize, bool)>> {
556            let signed = headers.get("x-signature").is_some();
557            Ok(Json((body.0.len(), signed)))
558        }
559        let t = App::new().route("/hook", post(verify)).into_test();
560        let res = t
561            .post_bytes_with("/hook", b"{\"raw\": 1}", &[("x-signature", "abc")])
562            .await;
563        assert_eq!(res.status().as_u16(), 200);
564        assert_eq!(res.json::<(usize, bool)>(), (10, true));
565    }
566
567    #[tokio::test]
568    async fn raw_body_drains_a_stream_route_transparently() {
569        use crate::prelude::*;
570        async fn len(body: RawBody) -> Result<Json<usize>> {
571            Ok(Json(body.0.len()))
572        }
573        let t = App::new().route("/up", post(len).stream_body()).into_test();
574        let payload = vec![b'x'; 100]; // > one 13-byte test frame
575        let res = t.post_bytes("/up", &payload).await;
576        assert_eq!(res.json::<usize>(), 100);
577    }
578
579    #[tokio::test]
580    async fn json_body_deserializes_and_bad_json_is_422() {
581        #[derive(serde::Deserialize)]
582        struct NewTodo {
583            title: String,
584        }
585        let mut c = ctx("/todos", r#"{"title":"x"}"#);
586        let Json(t): Json<NewTodo> = Json::from_request(&mut c).await.unwrap();
587        assert_eq!(t.title, "x");
588
589        let mut bad = ctx("/todos", r#"{"title":"#);
590        let err = Json::<NewTodo>::from_request(&mut bad).await.err().unwrap();
591        assert_eq!(err.code(), "JC0422");
592    }
593
594    /// Build a stream-lane RequestCtx directly, optionally capping it with
595    /// `Limited` — the in-process analogue of the serve-time stream lane,
596    /// without a socket. Frames the body in one chunk; that is enough for the
597    /// caching/limit unit tests (frame straddling is exercised by TestApp).
598    fn stream_ctx(body: &[u8], limit: Option<usize>) -> RequestCtx {
599        use http_body_util::BodyExt;
600        use http_body_util::combinators::UnsyncBoxBody;
601        let req = http::Request::builder().uri("/up").body(()).unwrap();
602        let (parts, ()) = req.into_parts();
603        let bytes = Bytes::copy_from_slice(body);
604        let lane: StreamLane = match limit {
605            Some(limit) => {
606                let limited = http_body_util::Limited::new(
607                    http_body_util::Full::<Bytes>::new(bytes).map_err(
608                        |never| -> Box<dyn std::error::Error + Send + Sync> { match never {} },
609                    ),
610                    limit,
611                );
612                UnsyncBoxBody::new(limited.map_err(Into::into))
613            }
614            None => {
615                let full = http_body_util::Full::<Bytes>::new(bytes);
616                UnsyncBoxBody::new(full.map_err(
617                    |never| -> Box<dyn std::error::Error + Send + Sync> { match never {} },
618                ))
619            }
620        };
621        RequestCtx::with_lane(
622            parts,
623            BodyLane::Stream(Some(lane)),
624            DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
625        )
626    }
627
628    #[tokio::test]
629    async fn stream_routes_deliver_the_body_and_enforce_the_limit() {
630        use crate::prelude::*;
631        async fn echo(Json(v): Json<serde_json::Value>) -> Result<Json<serde_json::Value>> {
632            Ok(Json(v))
633        }
634        let t = App::new()
635            .route("/up", post(echo).stream_body().body_limit(64))
636            .into_test();
637        // Json over a STREAM lane drains transparently.
638        let res = t.post_json("/up", &serde_json::json!({"k": "v"})).await;
639        assert_eq!(res.status().as_u16(), 200);
640        // Cumulative limit still applies on the stream lane: oversize → 413.
641        let big = serde_json::json!({"k": "x".repeat(200)});
642        let res = t.post_json("/up", &big).await;
643        assert_eq!(res.status().as_u16(), 413, "body: {}", res.text());
644    }
645
646    #[tokio::test]
647    async fn drain_body_twice_caches_the_stream_bytes() {
648        // The caching contract: a stream lane is drained once and cached back
649        // into Buffered, so a SECOND extractor on the same request keeps working
650        // instead of seeing an already-consumed stream.
651        use bytes::Bytes;
652        let mut c = stream_ctx(br#"{"k":"v"}"#, None);
653        let first = c.drain_body().await.unwrap();
654        assert_eq!(first, Bytes::from_static(br#"{"k":"v"}"#));
655        let second = c.drain_body().await.unwrap();
656        assert_eq!(second, first, "second drain returns the cached bytes");
657    }
658
659    #[tokio::test]
660    async fn stream_lane_over_limit_maps_to_413() {
661        // A stream lane whose Limited cap trips mid-drain surfaces as 413,
662        // exactly like the buffered read path.
663        let mut c = stream_ctx(&[b'x'; 200], Some(64));
664        let err = c.drain_body().await.err().unwrap();
665        assert_eq!(err.code(), "JC0413");
666    }
667
668    #[tokio::test]
669    async fn limit_trips_through_the_timed_recv_wrapper_still_map_to_413() {
670        // The serve-time lane wraps `Limited` in `TimedRecvBody` (the per-frame
671        // read-deadline guard); only the unwrapped `Limited` is covered above.
672        // If `TimedRecvBody`'s `map_err(Into::into)` ever double-boxed the
673        // error, `downcast_ref::<LengthLimitError>()` in `map_stream_error`
674        // would miss it and 413s would silently degrade to 400s. Build the
675        // exact serve.rs lane shape and assert the cap still maps to 413.
676        use crate::serve::TimedRecvBody;
677        use http_body_util::BodyExt;
678        use http_body_util::combinators::UnsyncBoxBody;
679        use std::time::Duration;
680
681        let req = http::Request::builder().uri("/up").body(()).unwrap();
682        let (parts, ()) = req.into_parts();
683        let over_limit_body = http_body_util::Full::<Bytes>::new(Bytes::from_static(&[b'x'; 200]))
684            .map_err(|never| -> Box<dyn std::error::Error + Send + Sync> { match never {} });
685        let lane: StreamLane = UnsyncBoxBody::new(TimedRecvBody::new(
686            http_body_util::Limited::new(over_limit_body, 64),
687            Duration::from_secs(5),
688        ));
689        let mut c = RequestCtx::with_lane(
690            parts,
691            BodyLane::Stream(Some(lane)),
692            DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
693        );
694        let err = c.drain_body().await.err().unwrap();
695        assert_eq!(err.code(), "JC0413");
696        assert_eq!(err.status().as_u16(), 413);
697    }
698}