tork_core/extract/
body.rs1use bytes::{BufMut, Bytes, BytesMut};
4use http_body_util::BodyExt;
5use serde::de::DeserializeOwned;
6
7use crate::body::ReqBody;
8use crate::constants::MAX_BODY_BYTES;
9use crate::error::{Error, Result};
10use crate::extract::{FromRequest, RequestContext};
11use crate::response::Json;
12
13pub(crate) const MAX_JSON_NESTING: usize = 128;
15
16#[derive(Clone, Copy)]
19pub(crate) struct AppBodyLimit(pub(crate) usize);
20
21pub(crate) fn configured_body_limit(ctx: &RequestContext) -> usize {
23 ctx.state()
24 .get::<AppBodyLimit>()
25 .map(|limit| limit.0)
26 .unwrap_or(MAX_BODY_BYTES)
27}
28
29impl<T> FromRequest for Json<T>
40where
41 T: DeserializeOwned + Send,
42{
43 fn from_request(
44 ctx: &RequestContext,
45 ) -> impl std::future::Future<Output = Result<Self>> + Send {
46 let taken = ctx.take_body();
47 let limit = configured_body_limit(ctx);
48 async move {
49 let body = taken?;
50 let bytes = read_body_capped_with(body, limit).await?;
51 ensure_json_depth_within_limit(&bytes)?;
52 let value = serde_json::from_slice::<T>(&bytes)
53 .map_err(|_| Error::unprocessable("request body is not valid JSON"))?;
54 Ok(Json(value))
55 }
56 }
57}
58
59pub(crate) fn ensure_json_depth_within_limit(bytes: &[u8]) -> Result<()> {
61 let mut depth = 0usize;
62 let mut in_string = false;
63 let mut escaped = false;
64
65 for byte in bytes {
66 if in_string {
67 if escaped {
68 escaped = false;
69 continue;
70 }
71 match byte {
72 b'\\' => escaped = true,
73 b'"' => in_string = false,
74 _ => {}
75 }
76 continue;
77 }
78
79 match byte {
80 b'"' => in_string = true,
81 b'{' | b'[' => {
82 depth += 1;
83 if depth > MAX_JSON_NESTING {
84 return Err(Error::bad_request("request body is too deeply nested"));
85 }
86 }
87 b'}' | b']' => depth = depth.saturating_sub(1),
88 _ => {}
89 }
90 }
91
92 Ok(())
93}
94
95pub(crate) async fn read_body_capped_with(mut body: ReqBody, limit: usize) -> Result<Bytes> {
102 let mut buffer = BytesMut::new();
103
104 while let Some(frame) = body.frame().await {
105 let frame = frame.map_err(map_body_error)?;
106
107 if let Ok(data) = frame.into_data() {
108 if buffer.len() + data.len() > limit {
109 return Err(Error::bad_request("request body is too large"));
110 }
111 buffer.put(data);
112 }
113 }
114
115 Ok(buffer.freeze())
116}
117
118fn map_body_error(error: crate::body::BoxError) -> Error {
119 match error.downcast::<Error>() {
120 Ok(error) => *error,
121 Err(_) => Error::bad_request("request body could not be read"),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::body::box_body;
129 use crate::extract::PathParams;
130 use crate::state::StateMap;
131 use http_body_util::Full;
132 use serde::Deserialize;
133 use std::sync::Arc;
134
135 #[derive(Debug, Deserialize, PartialEq)]
136 struct Payload {
137 name: String,
138 }
139
140 fn context(body: Bytes) -> RequestContext {
141 let head = http::Request::new(()).into_parts().0;
142 RequestContext::new(
143 head,
144 PathParams::new(),
145 Arc::new(StateMap::new()),
146 box_body(Full::new(body)),
147 )
148 }
149
150 #[tokio::test]
151 async fn reads_body_within_limit() {
152 let body = box_body(Full::new(Bytes::from_static(b"hello")));
153
154 let bytes = read_body_capped_with(body, MAX_BODY_BYTES).await.unwrap();
155 assert_eq!(bytes, Bytes::from_static(b"hello"));
156 }
157
158 #[tokio::test]
159 async fn rejects_body_over_limit() {
160 let oversized = vec![b'x'; MAX_BODY_BYTES + 1];
161 let body = box_body(Full::new(Bytes::from(oversized)));
162
163 let error = read_body_capped_with(body, MAX_BODY_BYTES).await.unwrap_err();
164 assert_eq!(error.kind(), crate::error::ErrorKind::BadRequest);
165 assert_eq!(error.message(), "request body is too large");
166 }
167
168 #[tokio::test]
169 async fn preserves_payload_too_large_errors_from_the_body() {
170 let body = crate::body::box_body(http_body_util::StreamBody::new(
171 futures_util::stream::iter(vec![
172 Ok::<_, crate::body::BoxError>(http_body::Frame::data(Bytes::from_static(
173 b"hello",
174 ))),
175 Err::<http_body::Frame<Bytes>, _>(Box::new(Error::payload_too_large(
176 "request body too large",
177 )) as crate::body::BoxError),
178 ]),
179 ));
180
181 let error = read_body_capped_with(body, MAX_BODY_BYTES).await.unwrap_err();
182 assert_eq!(error.kind(), crate::error::ErrorKind::PayloadTooLarge);
183 assert_eq!(error.message(), "request body too large");
184 }
185
186 #[tokio::test]
187 async fn json_extractor_accepts_valid_json() {
188 let ctx = context(Bytes::from_static(br#"{"name":"tork"}"#));
189
190 let Json(payload) = <Json<Payload> as FromRequest>::from_request(&ctx)
191 .await
192 .unwrap();
193 assert_eq!(
194 payload,
195 Payload {
196 name: "tork".to_owned()
197 }
198 );
199 }
200
201 #[tokio::test]
202 async fn json_extractor_rejects_invalid_json_shape() {
203 let ctx = context(Bytes::from_static(br#"{"name":1}"#));
204
205 let error = match <Json<Payload> as FromRequest>::from_request(&ctx).await {
206 Ok(_) => panic!("expected invalid JSON shape to fail"),
207 Err(error) => error,
208 };
209 assert_eq!(error.kind(), crate::error::ErrorKind::Unprocessable);
210 assert_eq!(error.message(), "request body is not valid JSON");
211 }
212
213 #[tokio::test]
214 async fn json_extractor_rejects_consumed_body() {
215 let ctx = context(Bytes::from_static(br#"{"name":"tork"}"#));
216 let _ = ctx.take_body().unwrap();
217
218 let error = match <Json<Payload> as FromRequest>::from_request(&ctx).await {
219 Ok(_) => panic!("expected consumed body to fail"),
220 Err(error) => error,
221 };
222 assert_eq!(error.kind(), crate::error::ErrorKind::BadRequest);
223 assert_eq!(error.message(), "request body has already been consumed");
224 }
225
226 #[test]
227 fn json_depth_guard_rejects_payloads_beyond_the_cap() {
228 let too_deep = format!(
229 "{}0{}",
230 "[".repeat(MAX_JSON_NESTING + 1),
231 "]".repeat(MAX_JSON_NESTING + 1)
232 );
233 let error = ensure_json_depth_within_limit(too_deep.as_bytes()).unwrap_err();
234 assert_eq!(error.kind(), crate::error::ErrorKind::BadRequest);
235 assert_eq!(error.message(), "request body is too deeply nested");
236 }
237
238 #[test]
239 fn json_depth_guard_ignores_brackets_inside_strings() {
240 let payload = br#"{"name":"[[[[not nesting]]]]","values":[1,2,3]}"#;
241 ensure_json_depth_within_limit(payload).unwrap();
242 }
243}