Skip to main content

kael_http_client/
async_body.rs

1use std::{
2    io::{Cursor, Read},
3    pin::Pin,
4    task::Poll,
5};
6
7use bytes::Bytes;
8use futures::AsyncRead;
9use http_body::{Body, Frame, SizeHint};
10
11/// An HTTP body backed by empty state, in-memory bytes, or an asynchronous reader.
12///
13/// The implementation is based on isahc's `AsyncBody` design.
14pub struct AsyncBody(Inner);
15
16enum Inner {
17    Empty,
18    Bytes(std::io::Cursor<Bytes>),
19    AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
20}
21
22impl AsyncBody {
23    /// Create a new empty body.
24    ///
25    /// An empty body represents the *absence* of a body, which is semantically
26    /// different than the presence of a body of zero length.
27    pub fn empty() -> Self {
28        Self(Inner::Empty)
29    }
30    /// Create a streaming body that reads from the given reader.
31    pub fn from_reader<R>(read: R) -> Self
32    where
33        R: AsyncRead + Send + Sync + 'static,
34    {
35        Self(Inner::AsyncReader(Box::pin(read)))
36    }
37
38    /// Creates an in-memory body from shared bytes.
39    pub fn from_bytes(bytes: Bytes) -> Self {
40        Self(Inner::Bytes(Cursor::new(bytes)))
41    }
42
43    /// Read the body into memory up to `max_bytes`, returning an error before the
44    /// buffer can grow beyond the caller's trust boundary.
45    pub async fn read_to_end_limited(&mut self, max_bytes: usize) -> std::io::Result<Vec<u8>> {
46        use futures::AsyncReadExt as _;
47
48        let mut output = Vec::new();
49        let mut chunk = [0u8; 8192];
50        loop {
51            let read = self.read(&mut chunk).await?;
52            if read == 0 {
53                return Ok(output);
54            }
55            let next_len = output.len().checked_add(read).ok_or_else(|| {
56                std::io::Error::new(std::io::ErrorKind::OutOfMemory, "HTTP body size overflow")
57            })?;
58            if next_len > max_bytes {
59                return Err(std::io::Error::new(
60                    std::io::ErrorKind::InvalidData,
61                    format!("HTTP body exceeds {max_bytes} byte limit"),
62                ));
63            }
64            output.try_reserve(read).map_err(|error| {
65                std::io::Error::new(
66                    std::io::ErrorKind::OutOfMemory,
67                    format!("could not reserve HTTP body buffer: {error}"),
68                )
69            })?;
70            output.extend_from_slice(&chunk[..read]);
71        }
72    }
73}
74
75impl Default for AsyncBody {
76    fn default() -> Self {
77        Self(Inner::Empty)
78    }
79}
80
81impl From<()> for AsyncBody {
82    fn from(_: ()) -> Self {
83        Self(Inner::Empty)
84    }
85}
86
87impl From<Bytes> for AsyncBody {
88    fn from(bytes: Bytes) -> Self {
89        Self::from_bytes(bytes)
90    }
91}
92
93impl From<Vec<u8>> for AsyncBody {
94    fn from(body: Vec<u8>) -> Self {
95        Self::from_bytes(body.into())
96    }
97}
98
99impl From<String> for AsyncBody {
100    fn from(body: String) -> Self {
101        Self::from_bytes(body.into())
102    }
103}
104
105impl From<&'static [u8]> for AsyncBody {
106    #[inline]
107    fn from(s: &'static [u8]) -> Self {
108        Self::from_bytes(Bytes::from_static(s))
109    }
110}
111
112impl From<&'static str> for AsyncBody {
113    #[inline]
114    fn from(s: &'static str) -> Self {
115        Self::from_bytes(Bytes::from_static(s.as_bytes()))
116    }
117}
118
119#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
120impl TryFrom<reqwest::Body> for AsyncBody {
121    type Error = anyhow::Error;
122
123    fn try_from(value: reqwest::Body) -> Result<Self, Self::Error> {
124        value
125            .as_bytes()
126            .ok_or_else(|| anyhow::anyhow!("Underlying data is a stream"))
127            .map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes)))
128    }
129}
130
131impl<T: Into<Self>> From<Option<T>> for AsyncBody {
132    fn from(body: Option<T>) -> Self {
133        match body {
134            Some(body) => body.into(),
135            None => Self::empty(),
136        }
137    }
138}
139
140impl futures::AsyncRead for AsyncBody {
141    fn poll_read(
142        self: Pin<&mut Self>,
143        cx: &mut std::task::Context<'_>,
144        buf: &mut [u8],
145    ) -> std::task::Poll<std::io::Result<usize>> {
146        let inner = &mut self.get_mut().0;
147        match inner {
148            Inner::Empty => Poll::Ready(Ok(0)),
149            // Blocking call is over an in-memory buffer
150            Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
151            Inner::AsyncReader(async_reader) => {
152                AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
153            }
154        }
155    }
156}
157
158impl Body for AsyncBody {
159    type Data = Bytes;
160    type Error = std::io::Error;
161
162    fn poll_frame(
163        mut self: Pin<&mut Self>,
164        cx: &mut std::task::Context<'_>,
165    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
166        let mut buffer = vec![0; 8192];
167        match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
168            Poll::Ready(Ok(0)) => Poll::Ready(None),
169            Poll::Ready(Ok(n)) => {
170                let data = Bytes::copy_from_slice(&buffer[..n]);
171                Poll::Ready(Some(Ok(Frame::data(data))))
172            }
173            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
174            Poll::Pending => Poll::Pending,
175        }
176    }
177
178    fn is_end_stream(&self) -> bool {
179        match &self.0 {
180            Inner::Empty => true,
181            Inner::Bytes(cursor) => cursor.position() >= cursor.get_ref().len() as u64,
182            Inner::AsyncReader(_) => false,
183        }
184    }
185
186    fn size_hint(&self) -> SizeHint {
187        match &self.0 {
188            Inner::Empty => SizeHint::with_exact(0),
189            Inner::Bytes(cursor) => {
190                let remaining = (cursor.get_ref().len() as u64).saturating_sub(cursor.position());
191                SizeHint::with_exact(remaining)
192            }
193            Inner::AsyncReader(_) => SizeHint::default(),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use futures::executor::block_on;
201    use http_body::Body as _;
202
203    use super::*;
204
205    #[test]
206    fn bounded_read_and_size_hint_track_remaining_bytes() {
207        let mut body = AsyncBody::from(vec![1, 2, 3]);
208        assert_eq!(body.size_hint().exact(), Some(3));
209        let bytes = block_on(body.read_to_end_limited(3)).unwrap();
210        assert_eq!(bytes, [1, 2, 3]);
211        assert!(body.is_end_stream());
212
213        let mut oversized = AsyncBody::from(vec![0; 4]);
214        assert!(block_on(oversized.read_to_end_limited(3)).is_err());
215    }
216}