1use crate::dep::DepResolver;
5use crate::error::{Error, Result};
6use crate::response::Json;
7use bytes::Bytes;
8use serde::de::DeserializeOwned;
9use std::future::Future;
10
11pub(crate) type StreamLane =
15 http_body_util::combinators::UnsyncBoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
16
17pub(crate) enum BodyLane {
21 Buffered(Bytes),
22 Stream(Option<StreamLane>),
24}
25
26#[derive(Clone, Copy, Debug)]
30pub struct ClientAddr(pub std::net::SocketAddr);
31
32pub struct RequestCtx {
35 pub(crate) parts: http::request::Parts,
36 pub(crate) body: BodyLane,
37 pub(crate) params: Vec<(String, String)>,
39 pub(crate) deps: DepResolver,
40 pub(crate) is_task: bool,
43}
44
45impl RequestCtx {
46 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 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 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 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 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 pub fn peer_addr(&self) -> Option<std::net::SocketAddr> {
116 self.parts.extensions.get::<ClientAddr>().map(|c| c.0)
117 }
118
119 pub fn take_extension<T: Send + Sync + 'static>(&mut self) -> Option<T> {
123 self.parts.extensions.remove::<T>()
124 }
125}
126
127pub(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
146pub trait FromRequest: Sized + Send {
149 fn from_request(ctx: &mut RequestCtx) -> impl Future<Output = Result<Self>> + Send;
150}
151
152pub struct Path<T>(pub T);
158
159#[doc(hidden)]
163pub mod sealed {
164 pub trait Sealed {}
165}
166
167pub 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#[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 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
262impl<A: PathParam, B: PathParam, C: PathParam, D: PathParam> FromRequest for Path<(A, B, C, D)> {
263 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
264 if ctx.is_task {
265 return Err(Error::task_context());
266 }
267 let [a, b, c, d] = take_params::<4>(ctx)?;
268 Ok(Path((
269 A::parse_param(&a.0, &a.1)?,
270 B::parse_param(&b.0, &b.1)?,
271 C::parse_param(&c.0, &c.1)?,
272 D::parse_param(&d.0, &d.1)?,
273 )))
274 }
275}
276
277impl<A: PathParam, B: PathParam, C: PathParam, D: PathParam, E: PathParam> FromRequest
278 for Path<(A, B, C, D, E)>
279{
280 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
281 if ctx.is_task {
282 return Err(Error::task_context());
283 }
284 let [a, b, c, d, e] = take_params::<5>(ctx)?;
285 Ok(Path((
286 A::parse_param(&a.0, &a.1)?,
287 B::parse_param(&b.0, &b.1)?,
288 C::parse_param(&c.0, &c.1)?,
289 D::parse_param(&d.0, &d.1)?,
290 E::parse_param(&e.0, &e.1)?,
291 )))
292 }
293}
294
295impl<A: PathParam, B: PathParam, C: PathParam, D: PathParam, E: PathParam, F: PathParam> FromRequest
296 for Path<(A, B, C, D, E, F)>
297{
298 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
299 if ctx.is_task {
300 return Err(Error::task_context());
301 }
302 let [a, b, c, d, e, f] = take_params::<6>(ctx)?;
303 Ok(Path((
304 A::parse_param(&a.0, &a.1)?,
305 B::parse_param(&b.0, &b.1)?,
306 C::parse_param(&c.0, &c.1)?,
307 D::parse_param(&d.0, &d.1)?,
308 E::parse_param(&e.0, &e.1)?,
309 F::parse_param(&f.0, &f.1)?,
310 )))
311 }
312}
313
314fn take_params<const N: usize>(ctx: &RequestCtx) -> Result<[(String, String); N]> {
317 if ctx.params.len() < N {
318 return Err(Error::internal(format!(
319 "route captures {} path parameter(s) but the handler expects {N}",
320 ctx.params.len()
321 )));
322 }
323 Ok(std::array::from_fn(|i| ctx.params[i].clone()))
324}
325
326pub struct PathParams(Vec<(String, String)>);
335
336impl PathParams {
337 pub fn get(&self, name: &str) -> Option<&str> {
340 self.0
341 .iter()
342 .find(|(k, _)| k == name)
343 .map(|(_, v)| v.as_str())
344 }
345}
346
347impl FromRequest for PathParams {
348 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
349 if ctx.is_task {
350 return Err(Error::task_context());
351 }
352 Ok(PathParams(ctx.params.clone()))
353 }
354}
355
356pub struct Query<T>(pub T);
358
359impl<T: DeserializeOwned + Send> FromRequest for Query<T> {
360 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
361 if ctx.is_task {
362 return Err(Error::task_context());
363 }
364 let q = ctx.parts.uri.query().unwrap_or("");
365 serde_urlencoded::from_str::<T>(q)
366 .map(Query)
367 .map_err(|e| Error::bad_request(format!("invalid query string: {e}")))
368 }
369}
370
371impl<T: DeserializeOwned + Send> FromRequest for Json<T> {
372 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
373 if ctx.is_task {
374 return Err(Error::task_context());
375 }
376 let body = ctx.drain_body().await?;
377 serde_json::from_slice::<T>(&body)
378 .map(Json)
379 .map_err(|e| Error::unprocessable(format!("invalid JSON body: {e}")))
380 }
381}
382
383pub struct Headers(pub(crate) http::HeaderMap);
385
386impl Headers {
387 pub fn get(&self, name: &str) -> Option<&str> {
389 self.0.get(name).and_then(|v| v.to_str().ok())
390 }
391}
392
393impl FromRequest for Headers {
394 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
395 if ctx.is_task {
396 return Err(Error::task_context());
397 }
398 Ok(Headers(ctx.headers().clone()))
399 }
400}
401
402pub struct RawBody(pub Bytes);
407
408impl FromRequest for RawBody {
409 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
410 if ctx.is_task {
411 return Err(Error::task_context());
412 }
413 Ok(RawBody(ctx.drain_body().await?))
414 }
415}
416
417impl<T: FromRequest> FromRequest for Option<T> {
424 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
425 Ok(T::from_request(ctx).await.ok())
426 }
427}
428
429impl<T: FromRequest> FromRequest for Result<T, Error> {
439 async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
440 Ok(T::from_request(ctx).await)
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::dep::DepEnv;
448 use std::sync::Arc;
449
450 fn ctx(uri: &str, body: &str) -> RequestCtx {
451 let req = http::Request::builder()
452 .method(http::Method::GET)
453 .uri(uri)
454 .body(())
455 .unwrap();
456 let (parts, ()) = req.into_parts();
457 RequestCtx::new(
458 parts,
459 Bytes::from(body.to_string()),
460 DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
461 )
462 }
463
464 #[tokio::test]
465 async fn peer_addr_is_none_without_a_socket_and_readable_when_set() {
466 let mut c = ctx("/x", "");
467 assert!(c.peer_addr().is_none());
468 let addr: std::net::SocketAddr = "203.0.113.7:5000".parse().unwrap();
469 c.parts.extensions.insert(crate::extract::ClientAddr(addr));
470 assert_eq!(c.peer_addr(), Some(addr));
471 }
472
473 #[tokio::test]
474 async fn path_extracts_typed_param() {
475 let mut c = ctx("/todos/42", "");
476 c.params.push(("id".into(), "42".into()));
477 let Path(id): Path<i64> = Path::<i64>::from_request(&mut c).await.unwrap();
478 assert_eq!(id, 42);
479 }
480
481 #[test]
482 fn ctx_param_reads_a_named_captured_param() {
483 let mut c = ctx("/x", "");
488 c.params.push(("club_id".into(), "42".into()));
489 c.params.push(("id".into(), "7".into()));
490 assert_eq!(c.param("club_id"), Some("42"));
491 assert_eq!(c.param("id"), Some("7"));
492 assert_eq!(c.param("missing"), None);
493 }
494
495 #[tokio::test]
496 async fn path_params_reads_named_and_rejects_task() {
497 let mut c = ctx("/x", "");
503 c.params.push(("club_id".into(), "42".into()));
504 c.params.push(("id".into(), "7".into()));
505 let p = PathParams::from_request(&mut c).await.unwrap();
506 assert_eq!(p.get("club_id"), Some("42"));
507 assert_eq!(p.get("id"), Some("7"));
508 assert_eq!(p.get("missing"), None);
509
510 let mut task = ctx("/x", "");
512 task.is_task = true;
513 let err = PathParams::from_request(&mut task).await.err().unwrap();
514 assert_eq!(err.code(), "JC1003");
515 }
516
517 #[tokio::test]
518 async fn path_with_wrong_type_is_400() {
519 let mut c = ctx("/todos/abc", "");
520 c.params.push(("id".into(), "abc".into()));
521 let err = Path::<i64>::from_request(&mut c).await.err().unwrap();
522 assert_eq!(err.code(), "JC0400");
523 }
524
525 #[tokio::test]
526 async fn path_missing_param_is_500() {
527 let mut c = ctx("/todos", "");
530 let err = Path::<i64>::from_request(&mut c).await.err().unwrap();
531 assert_eq!(err.code(), "JC0500");
532 }
533
534 #[tokio::test]
535 async fn query_deserializes_struct() {
536 #[derive(serde::Deserialize)]
537 struct Page {
538 limit: u32,
539 offset: u32,
540 }
541 let mut c = ctx("/todos?limit=10&offset=20", "");
542 let Query(p): Query<Page> = Query::from_request(&mut c).await.unwrap();
543 assert_eq!((p.limit, p.offset), (10, 20));
544 }
545
546 #[tokio::test]
547 async fn option_extractor_yields_none_on_failure_and_some_on_success() {
548 #[derive(serde::Deserialize)]
552 struct P {
553 n: i64,
554 }
555 async fn probe(q: Option<Query<P>>) -> Result<Json<Option<i64>>> {
556 Ok(Json(q.map(|Query(p)| p.n)))
557 }
558 let t = crate::App::new()
559 .route("/probe", crate::get(probe))
560 .into_test();
561 assert_eq!(t.get("/probe?n=7").await.text(), "7");
562 assert_eq!(t.get("/probe").await.text(), "null");
564 assert_eq!(t.get("/probe?n=not-a-number").await.text(), "null");
565 }
566
567 #[tokio::test]
568 async fn result_extractor_preserves_the_inner_error() {
569 struct Gate;
575 impl FromRequest for Gate {
576 async fn from_request(_ctx: &mut RequestCtx) -> Result<Self> {
577 Err(Error::forbidden())
578 }
579 }
580 async fn probe(gate: Result<Gate, Error>) -> Result<Json<&'static str>> {
581 let _gate = gate?;
582 Ok(Json("open"))
583 }
584 let t = crate::App::new()
585 .route("/probe", crate::get(probe))
586 .into_test();
587 let res = t.get("/probe").await;
588 assert_eq!(
589 res.status().as_u16(),
590 403,
591 "the inner error's status survives; body: {}",
592 res.text()
593 );
594 assert!(res.text().contains("JC0403"), "body: {}", res.text());
595 let mut c = ctx("/x", "");
597 let ok = <Result<Headers, Error> as FromRequest>::from_request(&mut c)
598 .await
599 .expect("outer extraction never fails");
600 assert!(ok.is_ok(), "success is Ok(Ok(_))");
601 }
602
603 #[tokio::test]
604 async fn single_path_param_binds_the_leaf_segment() {
605 use crate::prelude::*;
606 async fn show(Path(id): Path<i64>) -> Result<Json<i64>> {
607 Ok(Json(id))
608 }
609 let t = App::new()
610 .mount(
611 "/ws/{ws}",
612 Module::new("leads").route("/leads/{id}", get(show)),
613 )
614 .into_test();
615 assert_eq!(
616 t.get("/ws/7/leads/42").await.json::<i64>(),
617 42,
618 "leaf param, not mount param"
619 );
620 }
621
622 #[tokio::test]
623 async fn tuples_still_read_root_to_leaf() {
624 use crate::prelude::*;
625 async fn pair(Path((ws, id)): Path<(i64, i64)>) -> Result<Json<(i64, i64)>> {
626 Ok(Json((ws, id)))
627 }
628 let t = App::new()
629 .mount(
630 "/ws/{ws}",
631 Module::new("leads").route("/leads/{id}", get(pair)),
632 )
633 .into_test();
634 assert_eq!(t.get("/ws/7/leads/42").await.json::<(i64, i64)>(), (7, 42));
635 }
636
637 #[tokio::test]
645 async fn deep_nested_triple_reads_root_to_leaf() {
646 use crate::prelude::*;
647 async fn triple(
648 Path((account_id, contact_id, id)): Path<(i64, i64, i64)>,
649 ) -> Result<Json<(i64, i64, i64)>> {
650 Ok(Json((account_id, contact_id, id)))
651 }
652 let t = App::new()
653 .mount(
654 "/accounts/{account_id}",
655 Module::new("contacts").mount(
656 "/contacts/{contact_id}",
657 Module::new("notes").route("/notes/{id}", get(triple)),
658 ),
659 )
660 .into_test();
661 assert_eq!(
662 t.get("/accounts/111/contacts/222/notes/999")
663 .await
664 .json::<(i64, i64, i64)>(),
665 (111, 222, 999),
666 "the 3-tuple must read account_id=111, contact_id=222, id=999 positionally"
667 );
668 }
669
670 #[tokio::test]
671 async fn path_param_macro_admits_custom_newtypes() {
672 use crate::prelude::*;
673 #[derive(Debug)]
674 struct LeadId(i64);
675 impl std::str::FromStr for LeadId {
676 type Err = std::num::ParseIntError;
677 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
678 Ok(LeadId(s.parse()?))
679 }
680 }
681 crate::path_param!(LeadId);
682 async fn show(Path(id): Path<LeadId>) -> Result<Json<i64>> {
683 Ok(Json(id.0))
684 }
685 let t = App::new().route("/leads/{id}", get(show)).into_test();
686 assert_eq!(t.get("/leads/42").await.json::<i64>(), 42);
687 }
688
689 #[tokio::test]
690 async fn raw_body_yields_exact_bytes_and_coexists_with_headers() {
691 use crate::prelude::*;
692 async fn verify(headers: Headers, body: RawBody) -> Result<Json<(usize, bool)>> {
693 let signed = headers.get("x-signature").is_some();
694 Ok(Json((body.0.len(), signed)))
695 }
696 let t = App::new().route("/hook", post(verify)).into_test();
697 let res = t
698 .post_bytes_with("/hook", b"{\"raw\": 1}", &[("x-signature", "abc")])
699 .await;
700 assert_eq!(res.status().as_u16(), 200);
701 assert_eq!(res.json::<(usize, bool)>(), (10, true));
702 }
703
704 #[tokio::test]
705 async fn raw_body_drains_a_stream_route_transparently() {
706 use crate::prelude::*;
707 async fn len(body: RawBody) -> Result<Json<usize>> {
708 Ok(Json(body.0.len()))
709 }
710 let t = App::new().route("/up", post(len).stream_body()).into_test();
711 let payload = vec![b'x'; 100]; let res = t.post_bytes("/up", &payload).await;
713 assert_eq!(res.json::<usize>(), 100);
714 }
715
716 #[tokio::test]
717 async fn json_body_deserializes_and_bad_json_is_422() {
718 #[derive(serde::Deserialize)]
719 struct NewTodo {
720 title: String,
721 }
722 let mut c = ctx("/todos", r#"{"title":"x"}"#);
723 let Json(t): Json<NewTodo> = Json::from_request(&mut c).await.unwrap();
724 assert_eq!(t.title, "x");
725
726 let mut bad = ctx("/todos", r#"{"title":"#);
727 let err = Json::<NewTodo>::from_request(&mut bad).await.err().unwrap();
728 assert_eq!(err.code(), "JC0422");
729 }
730
731 fn stream_ctx(body: &[u8], limit: Option<usize>) -> RequestCtx {
736 use http_body_util::BodyExt;
737 use http_body_util::combinators::UnsyncBoxBody;
738 let req = http::Request::builder().uri("/up").body(()).unwrap();
739 let (parts, ()) = req.into_parts();
740 let bytes = Bytes::copy_from_slice(body);
741 let lane: StreamLane = match limit {
742 Some(limit) => {
743 let limited = http_body_util::Limited::new(
744 http_body_util::Full::<Bytes>::new(bytes).map_err(
745 |never| -> Box<dyn std::error::Error + Send + Sync> { match never {} },
746 ),
747 limit,
748 );
749 UnsyncBoxBody::new(limited.map_err(Into::into))
750 }
751 None => {
752 let full = http_body_util::Full::<Bytes>::new(bytes);
753 UnsyncBoxBody::new(full.map_err(
754 |never| -> Box<dyn std::error::Error + Send + Sync> { match never {} },
755 ))
756 }
757 };
758 RequestCtx::with_lane(
759 parts,
760 BodyLane::Stream(Some(lane)),
761 DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
762 )
763 }
764
765 #[tokio::test]
766 async fn stream_routes_deliver_the_body_and_enforce_the_limit() {
767 use crate::prelude::*;
768 async fn echo(Json(v): Json<serde_json::Value>) -> Result<Json<serde_json::Value>> {
769 Ok(Json(v))
770 }
771 let t = App::new()
772 .route("/up", post(echo).stream_body().body_limit(64))
773 .into_test();
774 let res = t.post_json("/up", &serde_json::json!({"k": "v"})).await;
776 assert_eq!(res.status().as_u16(), 200);
777 let big = serde_json::json!({"k": "x".repeat(200)});
779 let res = t.post_json("/up", &big).await;
780 assert_eq!(res.status().as_u16(), 413, "body: {}", res.text());
781 }
782
783 #[tokio::test]
784 async fn drain_body_twice_caches_the_stream_bytes() {
785 use bytes::Bytes;
789 let mut c = stream_ctx(br#"{"k":"v"}"#, None);
790 let first = c.drain_body().await.unwrap();
791 assert_eq!(first, Bytes::from_static(br#"{"k":"v"}"#));
792 let second = c.drain_body().await.unwrap();
793 assert_eq!(second, first, "second drain returns the cached bytes");
794 }
795
796 #[tokio::test]
797 async fn stream_lane_over_limit_maps_to_413() {
798 let mut c = stream_ctx(&[b'x'; 200], Some(64));
801 let err = c.drain_body().await.err().unwrap();
802 assert_eq!(err.code(), "JC0413");
803 }
804
805 #[tokio::test]
806 async fn limit_trips_through_the_timed_recv_wrapper_still_map_to_413() {
807 use crate::serve::TimedRecvBody;
814 use http_body_util::BodyExt;
815 use http_body_util::combinators::UnsyncBoxBody;
816 use std::time::Duration;
817
818 let req = http::Request::builder().uri("/up").body(()).unwrap();
819 let (parts, ()) = req.into_parts();
820 let over_limit_body = http_body_util::Full::<Bytes>::new(Bytes::from_static(&[b'x'; 200]))
821 .map_err(|never| -> Box<dyn std::error::Error + Send + Sync> { match never {} });
822 let lane: StreamLane = UnsyncBoxBody::new(TimedRecvBody::new(
823 http_body_util::Limited::new(over_limit_body, 64),
824 Duration::from_secs(5),
825 ));
826 let mut c = RequestCtx::with_lane(
827 parts,
828 BodyLane::Stream(Some(lane)),
829 DepResolver::new(Arc::new(DepEnv::default()), Default::default()),
830 );
831 let err = c.drain_body().await.err().unwrap();
832 assert_eq!(err.code(), "JC0413");
833 assert_eq!(err.status().as_u16(), 413);
834 }
835}