Skip to main content

siderite_core/
body.rs

1//! Opaque HTTP body type.
2
3use crate::error::BodyError;
4
5/// Default cap for [`Body::into_bytes`], matching the `Json` and `Form` extractors.
6pub const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024;
7
8fn is_length_limit(err: &axum::Error) -> bool {
9    let mut source: Option<&(dyn std::error::Error + 'static)> = Some(err);
10    while let Some(current) = source {
11        if current.is::<http_body_util::LengthLimitError>() {
12            return true;
13        }
14        source = current.source();
15    }
16    false
17}
18
19/// An HTTP request or response body.
20#[derive(Debug, Default)]
21pub struct Body(axum::body::Body);
22
23impl Body {
24    /// An empty body.
25    pub fn empty() -> Self {
26        Self::default()
27    }
28
29    /// Collect the whole body into memory, up to [`DEFAULT_BODY_LIMIT`] bytes.
30    ///
31    /// Use [`Body::into_bytes_limited`] to pick a different cap.
32    ///
33    /// # Errors
34    /// Returns [`BodyError`] if the body stream fails or exceeds the limit
35    /// ([`BodyError::is_too_large`]).
36    pub async fn into_bytes(self) -> Result<Vec<u8>, BodyError> {
37        self.into_bytes_limited(DEFAULT_BODY_LIMIT).await
38    }
39
40    /// Collect the whole body into memory, failing once it exceeds `max_bytes`.
41    ///
42    /// # Errors
43    /// Returns [`BodyError`] if the body stream fails or exceeds `max_bytes`
44    /// ([`BodyError::is_too_large`]).
45    pub async fn into_bytes_limited(self, max_bytes: usize) -> Result<Vec<u8>, BodyError> {
46        if self.exact_len().is_some_and(|len| len > max_bytes as u64) {
47            return Err(BodyError::too_large(max_bytes));
48        }
49        match axum::body::to_bytes(self.0, max_bytes).await {
50            Ok(bytes) => Ok(bytes.to_vec()),
51            Err(err) if is_length_limit(&err) => Err(BodyError::too_large(max_bytes)),
52            Err(err) => Err(BodyError::failed(err.to_string())),
53        }
54    }
55
56    /// The body length when it is known up front (fully buffered bodies).
57    ///
58    /// `None` for streaming bodies (server-sent events, file streams, ...),
59    /// whose length is only known once they end.
60    pub fn exact_len(&self) -> Option<u64> {
61        axum::body::HttpBody::size_hint(&self.0).exact()
62    }
63
64    pub(crate) fn from_inner(inner: axum::body::Body) -> Self {
65        Self(inner)
66    }
67
68    pub(crate) fn into_inner(self) -> axum::body::Body {
69        self.0
70    }
71}
72
73impl From<Vec<u8>> for Body {
74    fn from(bytes: Vec<u8>) -> Self {
75        Self(axum::body::Body::from(bytes))
76    }
77}
78
79impl From<String> for Body {
80    fn from(text: String) -> Self {
81        Self(axum::body::Body::from(text))
82    }
83}
84
85impl From<&'static str> for Body {
86    fn from(text: &'static str) -> Self {
87        Self(axum::body::Body::from(text))
88    }
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used)]
93mod limit_tests {
94    use super::*;
95    use crate::error::ApiError;
96    use http::StatusCode;
97
98    fn streamed(chunks: usize, size: usize) -> Body {
99        let stream = futures_util::stream::iter(
100            (0..chunks).map(move |_| Ok::<_, std::io::Error>(vec![b'x'; size])),
101        );
102        Body::from_inner(axum::body::Body::from_stream(stream))
103    }
104
105    #[tokio::test]
106    async fn buffered_body_over_limit_is_too_large() {
107        let err = Body::from(vec![0u8; 11])
108            .into_bytes_limited(10)
109            .await
110            .unwrap_err();
111        assert!(err.is_too_large());
112        assert_eq!(ApiError::from(err).status(), StatusCode::PAYLOAD_TOO_LARGE);
113    }
114
115    #[tokio::test]
116    async fn streamed_body_over_limit_is_too_large() {
117        let err = streamed(4, 4).into_bytes_limited(10).await.unwrap_err();
118        assert!(err.is_too_large());
119    }
120
121    #[tokio::test]
122    async fn default_limit_applies_and_bodies_within_it_pass() {
123        assert_eq!(
124            streamed(2, 5).into_bytes_limited(10).await.unwrap().len(),
125            10
126        );
127        let err = streamed(DEFAULT_BODY_LIMIT / 1024 + 1, 1024)
128            .into_bytes()
129            .await
130            .unwrap_err();
131        assert!(err.is_too_large());
132    }
133}