Skip to main content

actix_http/body/
sized_stream.rs

1use std::{
2    error::Error as StdError,
3    pin::Pin,
4    task::{Context, Poll},
5};
6
7use bytes::Bytes;
8use futures_core::{ready, Stream};
9use pin_project_lite::pin_project;
10
11use super::{BodySize, InfallibleStream, MessageBody};
12
13pin_project! {
14    /// Known sized streaming response wrapper.
15    ///
16    /// This body implementation should be used if total size of stream is known. Data is sent as-is
17    /// without using chunked transfer encoding.
18    pub struct SizedStream<S> {
19        size: u64,
20        #[pin]
21        stream: S,
22    }
23}
24
25impl<S, E> SizedStream<S>
26where
27    S: Stream<Item = Result<Bytes, E>>,
28    E: Into<Box<dyn StdError>> + 'static,
29{
30    #[inline]
31    #[must_use]
32    pub fn new(size: u64, stream: S) -> Self {
33        SizedStream { size, stream }
34    }
35}
36
37impl<S> SizedStream<InfallibleStream<S>>
38where
39    S: Stream<Item = Bytes>,
40{
41    /// Creates a body from a stream of bytes that cannot yield errors.
42    ///
43    /// The caller must provide the total number of bytes in the stream.
44    ///
45    /// Empty chunks are skipped.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// # use actix_http::body::{SizedStream, InfallibleStream};
51    /// # use bytes::Bytes;
52    /// use futures_util::stream;
53    ///
54    /// let stream = stream::iter([Bytes::from_static(b"hello")]);
55    /// let body = SizedStream::from_infallible(5, stream);
56    /// ```
57    #[inline]
58    #[must_use]
59    pub fn from_infallible(size: u64, stream: S) -> Self {
60        SizedStream::new(size, InfallibleStream { stream })
61    }
62}
63
64impl<S, E> MessageBody for SizedStream<S>
65where
66    S: Stream<Item = Result<Bytes, E>>,
67    E: Into<Box<dyn StdError>> + 'static,
68{
69    type Error = E;
70
71    #[inline]
72    fn size(&self) -> BodySize {
73        BodySize::Sized(self.size)
74    }
75
76    /// Attempts to pull out the next value of the underlying [`Stream`].
77    ///
78    /// Empty values are skipped to prevent [`SizedStream`]'s transmission being
79    /// ended on a zero-length chunk, but rather proceed until the underlying
80    /// [`Stream`] ends.
81    fn poll_next(
82        mut self: Pin<&mut Self>,
83        cx: &mut Context<'_>,
84    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
85        loop {
86            let stream = self.as_mut().project().stream;
87
88            let chunk = match ready!(stream.poll_next(cx)) {
89                Some(Ok(ref bytes)) if bytes.is_empty() => continue,
90                val => val,
91            };
92
93            return Poll::Ready(chunk);
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use std::{convert::Infallible, pin::pin};
101
102    use actix_utils::future::poll_fn;
103    use futures_util::stream;
104    use static_assertions::{assert_impl_all, assert_not_impl_any};
105
106    use super::*;
107    use crate::body::to_bytes;
108
109    assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
110    assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
111    assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
112    assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
113    assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
114
115    assert_not_impl_any!(SizedStream<stream::Empty<Bytes>>: MessageBody);
116    assert_not_impl_any!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
117    // crate::Error is not Clone
118    assert_not_impl_any!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
119
120    #[actix_rt::test]
121    async fn infallible_stream() {
122        let stream = async_stream::stream! {
123            yield Bytes::new();
124            yield Bytes::from_static(b"1");
125            tokio::task::yield_now().await;
126            yield Bytes::new();
127            yield Bytes::from_static(b"2");
128            yield Bytes::new();
129        };
130        let body = SizedStream::from_infallible(2, stream);
131
132        assert_eq!(body.size(), BodySize::Sized(2));
133        assert_eq!(
134            to_bytes(body).await,
135            Ok::<_, Infallible>(Bytes::from_static(b"12"))
136        );
137    }
138
139    #[actix_rt::test]
140    async fn infallible_empty_stream() {
141        let body = SizedStream::from_infallible(0, stream::empty::<Bytes>());
142
143        assert_eq!(to_bytes(body).await, Ok::<_, Infallible>(Bytes::new()));
144    }
145
146    #[actix_rt::test]
147    async fn skips_empty_chunks() {
148        let body = SizedStream::new(
149            2,
150            stream::iter(
151                ["1", "", "2"]
152                    .iter()
153                    .map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
154            ),
155        );
156
157        let mut body = pin!(body);
158
159        assert_eq!(
160            poll_fn(|cx| body.as_mut().poll_next(cx))
161                .await
162                .unwrap()
163                .ok(),
164            Some(Bytes::from("1")),
165        );
166
167        assert_eq!(
168            poll_fn(|cx| body.as_mut().poll_next(cx))
169                .await
170                .unwrap()
171                .ok(),
172            Some(Bytes::from("2")),
173        );
174    }
175
176    #[actix_rt::test]
177    async fn read_to_bytes() {
178        let body = SizedStream::new(
179            2,
180            stream::iter(
181                ["1", "", "2"]
182                    .iter()
183                    .map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
184            ),
185        );
186
187        assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
188    }
189
190    #[actix_rt::test]
191    async fn stream_string_error() {
192        // `&'static str` does not impl `Error`
193        // but it does impl `Into<Box<dyn Error>>`
194
195        let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
196        assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
197
198        let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
199        assert!(matches!(to_bytes(body).await, Err("stringy error")));
200    }
201
202    #[actix_rt::test]
203    async fn stream_boxed_error() {
204        // `Box<dyn Error>` does not impl `Error`
205        // but it does impl `Into<Box<dyn Error>>`
206
207        let body = SizedStream::new(
208            0,
209            stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
210        );
211        assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
212
213        let body = SizedStream::new(
214            1,
215            stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
216        );
217        assert_eq!(
218            to_bytes(body).await.unwrap_err().to_string(),
219            "stringy error"
220        );
221    }
222}