Skip to main content

churust_core/
body.rs

1//! The response [`Body`]: either a fully-buffered `Bytes` payload or a lazy
2//! stream of byte chunks (for files, large/dynamic responses, SSE, etc.).
3
4use crate::error::{Error, Result};
5use bytes::{Bytes, BytesMut};
6use futures_util::stream::{Stream, StreamExt};
7use std::pin::Pin;
8
9/// A response body.
10///
11/// Construct buffered bodies with `From` (`Body::from(bytes)`,
12/// `"text".into()`), an empty body with [`Body::empty`], or a streaming body
13/// with [`Body::from_stream`]. The engine sends buffered bodies in one shot and
14/// streamed bodies frame-by-frame.
15pub enum Body {
16    /// A fully-buffered, in-memory body.
17    Bytes(Bytes),
18    /// A lazily-produced stream of byte chunks.
19    Stream(Pin<Box<dyn Stream<Item = Result<Bytes>> + Send + 'static>>),
20}
21
22impl Body {
23    /// An empty buffered body.
24    pub fn empty() -> Self {
25        Body::Bytes(Bytes::new())
26    }
27
28    /// Build a streaming body from any `Stream` of byte chunks. Chunk errors are
29    /// converted into [`Error`] and surface when the body is read/collected.
30    pub fn from_stream<S, E>(stream: S) -> Self
31    where
32        S: Stream<Item = std::result::Result<Bytes, E>> + Send + 'static,
33        E: std::fmt::Display,
34    {
35        let mapped =
36            stream.map(|chunk| chunk.map_err(|e| Error::internal(format!("body stream: {e}"))));
37        Body::Stream(Box::pin(mapped))
38    }
39
40    /// Borrow the buffered bytes, or `None` if this body is a stream. Used by
41    /// middleware that only post-processes already-materialized bodies.
42    pub fn as_bytes(&self) -> Option<&Bytes> {
43        match self {
44            Body::Bytes(b) => Some(b),
45            Body::Stream(_) => None,
46        }
47    }
48
49    /// Borrow the buffered bytes as a slice, or `None` for a stream.
50    pub fn as_slice(&self) -> Option<&[u8]> {
51        self.as_bytes().map(|b| b.as_ref())
52    }
53
54    /// True if this is a buffered, empty body. A stream is never reported empty
55    /// (its length is unknown until read).
56    pub fn is_empty(&self) -> bool {
57        matches!(self, Body::Bytes(b) if b.is_empty())
58    }
59
60    /// Collect the whole body into `Bytes` (returns the buffer directly when
61    /// already buffered, or drains the stream).
62    pub async fn into_bytes(self) -> Result<Bytes> {
63        match self {
64            Body::Bytes(b) => Ok(b),
65            Body::Stream(mut s) => {
66                let mut buf = BytesMut::new();
67                while let Some(chunk) = s.next().await {
68                    buf.extend_from_slice(&chunk?);
69                }
70                Ok(buf.freeze())
71            }
72        }
73    }
74}
75
76impl Default for Body {
77    fn default() -> Self {
78        Body::empty()
79    }
80}
81
82impl From<Bytes> for Body {
83    fn from(b: Bytes) -> Self {
84        Body::Bytes(b)
85    }
86}
87impl From<Vec<u8>> for Body {
88    fn from(b: Vec<u8>) -> Self {
89        Body::Bytes(Bytes::from(b))
90    }
91}
92impl From<String> for Body {
93    fn from(s: String) -> Self {
94        Body::Bytes(Bytes::from(s))
95    }
96}
97impl From<&'static str> for Body {
98    fn from(s: &'static str) -> Self {
99        Body::Bytes(Bytes::from_static(s.as_bytes()))
100    }
101}
102
103/// Compare a body to `Bytes` (true only when buffered and equal). Lets tests and
104/// callers assert on buffered bodies ergonomically.
105impl PartialEq<Bytes> for Body {
106    fn eq(&self, other: &Bytes) -> bool {
107        matches!(self, Body::Bytes(b) if b == other)
108    }
109}
110
111impl std::fmt::Debug for Body {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            Body::Bytes(b) => f.debug_tuple("Body::Bytes").field(b).finish(),
115            Body::Stream(_) => f.write_str("Body::Stream(..)"),
116        }
117    }
118}
119
120/// [`Body`] is an `http_body::Body`, so it can be handed to anything written
121/// against the `http`/`http-body` crates — which is what makes the `tower`
122/// adapter possible without a wrapper type at every boundary.
123impl http_body::Body for Body {
124    type Data = Bytes;
125    type Error = Error;
126
127    fn poll_frame(
128        mut self: Pin<&mut Self>,
129        cx: &mut std::task::Context<'_>,
130    ) -> std::task::Poll<Option<std::result::Result<http_body::Frame<Bytes>, Error>>> {
131        match &mut *self {
132            Body::Bytes(bytes) => {
133                if bytes.is_empty() {
134                    std::task::Poll::Ready(None)
135                } else {
136                    // Take the payload so the next poll reports end-of-stream
137                    // rather than yielding the same bytes forever.
138                    let out = std::mem::take(bytes);
139                    std::task::Poll::Ready(Some(Ok(http_body::Frame::data(out))))
140                }
141            }
142            Body::Stream(stream) => match stream.as_mut().poll_next(cx) {
143                std::task::Poll::Ready(Some(Ok(chunk))) => {
144                    std::task::Poll::Ready(Some(Ok(http_body::Frame::data(chunk))))
145                }
146                std::task::Poll::Ready(Some(Err(e))) => std::task::Poll::Ready(Some(Err(e))),
147                std::task::Poll::Ready(None) => std::task::Poll::Ready(None),
148                std::task::Poll::Pending => std::task::Poll::Pending,
149            },
150        }
151    }
152
153    fn size_hint(&self) -> http_body::SizeHint {
154        match self {
155            // Exact for a buffered body, so `Content-Length` can be set.
156            Body::Bytes(b) => http_body::SizeHint::with_exact(b.len() as u64),
157            // A stream's length is not known until it ends.
158            Body::Stream(_) => http_body::SizeHint::default(),
159        }
160    }
161
162    fn is_end_stream(&self) -> bool {
163        match self {
164            Body::Bytes(b) => b.is_empty(),
165            Body::Stream(_) => false,
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[tokio::test]
175    async fn buffered_round_trips() {
176        let body = Body::from(Bytes::from("hello"));
177        assert_eq!(body.as_slice(), Some(&b"hello"[..]));
178        assert!(!body.is_empty());
179        assert_eq!(body.into_bytes().await.unwrap(), Bytes::from("hello"));
180    }
181
182    #[tokio::test]
183    async fn empty_is_empty() {
184        assert!(Body::empty().is_empty());
185        assert_eq!(Body::empty().into_bytes().await.unwrap(), Bytes::new());
186    }
187
188    #[tokio::test]
189    async fn stream_collects_and_has_no_bytes_view() {
190        let chunks = futures_util::stream::iter(vec![
191            Ok::<_, std::io::Error>(Bytes::from("ab")),
192            Ok(Bytes::from("cd")),
193        ]);
194        let body = Body::from_stream(chunks);
195        assert!(body.as_bytes().is_none()); // streams expose no borrowed view
196        assert!(!body.is_empty());
197        assert_eq!(body.into_bytes().await.unwrap(), Bytes::from("abcd"));
198    }
199
200    #[test]
201    #[allow(clippy::cmp_owned)] // exercises From<String>; the owned String is the input under test, not a throwaway
202    fn partial_eq_bytes() {
203        assert!(Body::from("x".to_string()) == Bytes::from("x"));
204        let s = Body::from_stream(futures_util::stream::iter(vec![Ok::<_, std::io::Error>(
205            Bytes::new(),
206        )]));
207        assert!(!(s == Bytes::new())); // a stream never equals bytes
208    }
209}