Skip to main content

tork_core/extract/
body.rs

1//! Request-body extractors.
2
3use 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
13/// Maximum nesting depth accepted for a buffered JSON payload.
14pub(crate) const MAX_JSON_NESTING: usize = 128;
15
16/// The app-wide request-body size cap, stored in app state by
17/// [`App::max_request_body_size`](crate::App::max_request_body_size).
18#[derive(Clone, Copy)]
19pub(crate) struct AppBodyLimit(pub(crate) usize);
20
21/// Returns the configured request-body cap, falling back to [`MAX_BODY_BYTES`].
22pub(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
29/// Deserializes the request body as JSON.
30///
31/// The body is buffered with a size cap of [`MAX_BODY_BYTES`] to guard against
32/// memory-exhaustion attacks, then parsed into `T`.
33///
34/// # Errors
35///
36/// - `400 Bad Request` if the body was already consumed, exceeds the size cap,
37///   or could not be read.
38/// - `422 Unprocessable Entity` if the body is not valid JSON for `T`.
39impl<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
59/// Rejects deeply nested JSON before deserialization.
60pub(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
95/// Buffers a request body, rejecting payloads larger than `limit` bytes (the
96/// app-configured [`max_request_body_size`](crate::App::max_request_body_size), or
97/// [`MAX_BODY_BYTES`] by default).
98///
99/// The cap is enforced incrementally as frames arrive, so an oversized payload is
100/// rejected without buffering all of it first.
101pub(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}