Skip to main content

rama_http/layer/compression/stream/
body.rs

1use crate::HeaderMap;
2use crate::layer::compression::pin_project_cfg::pin_project_cfg;
3use crate::layer::util::compression::CompressionLevel;
4
5use compression_codecs::{
6    BrotliEncoder, EncodeV2, GzipEncoder, ZlibEncoder, ZstdEncoder,
7    brotli::params::EncoderParams as BrotliEncoderParams,
8};
9use compression_core::util;
10use rama_core::bytes::BytesMut;
11use rama_core::{
12    bytes::{Buf, Bytes},
13    error::BoxError,
14};
15
16use pin_project_lite::pin_project;
17use rama_http_types::StreamingBody;
18use rama_http_types::body::{Frame, SizeHint};
19use std::{
20    io,
21    pin::Pin,
22    task::{Context, Poll},
23};
24
25pin_project! {
26    /// Response body of [`StreamCompression`].
27    ///
28    /// [`StreamCompression`]: super::StreamCompression
29    pub struct StreamCompressionBody<B>
30    where
31        B: StreamingBody,
32    {
33        #[pin]
34        inner: BodyInner<B>,
35    }
36}
37
38impl<B> Default for StreamCompressionBody<B>
39where
40    B: StreamingBody + Default,
41{
42    fn default() -> Self {
43        Self {
44            inner: BodyInner::Identity {
45                inner: B::default(),
46            },
47        }
48    }
49}
50
51enum Encoder {
52    Gzip(GzipEncoder),
53    Deflate(ZlibEncoder),
54    Brotli(Box<BrotliEncoder>),
55    Zstd(ZstdEncoder),
56}
57
58impl EncodeV2 for Encoder {
59    fn encode(
60        &mut self,
61        input: &mut util::PartialBuffer<&[u8]>,
62        output: &mut util::WriteBuffer<'_>,
63    ) -> io::Result<()> {
64        match self {
65            Self::Gzip(e) => e.encode(input, output),
66            Self::Deflate(e) => e.encode(input, output),
67            Self::Brotli(e) => e.encode(input, output),
68            Self::Zstd(e) => e.encode(input, output),
69        }
70    }
71
72    fn flush(&mut self, output: &mut util::WriteBuffer<'_>) -> io::Result<bool> {
73        match self {
74            Self::Gzip(e) => e.flush(output),
75            Self::Deflate(e) => e.flush(output),
76            Self::Brotli(e) => e.flush(output),
77            Self::Zstd(e) => e.flush(output),
78        }
79    }
80
81    fn finish(&mut self, output: &mut util::WriteBuffer<'_>) -> io::Result<bool> {
82        match self {
83            Self::Gzip(e) => e.finish(output),
84            Self::Deflate(e) => e.finish(output),
85            Self::Brotli(e) => e.finish(output),
86            Self::Zstd(e) => e.finish(output),
87        }
88    }
89}
90
91struct CompressData {
92    encoder: Encoder,
93    output_buffer: BytesMut,
94    always_flush: bool,
95    state: CompressState,
96    pending_trailers: Option<HeaderMap>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100enum CompressState {
101    /// Reading data from inner body and compressing.
102    Reading,
103    /// Finishing compression after inner body is done.
104    Finishing,
105    /// Emitting buffered trailers.
106    Trailers,
107    /// Compression is complete.
108    Done,
109}
110
111pin_project_cfg! {
112    #[project = BodyInnerProj]
113    enum BodyInner<B>
114    where
115        B: StreamingBody,
116    {
117        Compress {
118            #[pin]
119            inner: B,
120            data: CompressData,
121        },
122        Identity {
123            #[pin]
124            inner: B,
125        },
126    }
127}
128
129impl CompressData {
130    const INTERNAL_BUF_CAPACITY: usize = 8096;
131
132    fn new(encoder: Encoder, always_flush: bool) -> Self {
133        Self {
134            encoder,
135            output_buffer: BytesMut::with_capacity(Self::INTERNAL_BUF_CAPACITY),
136            always_flush,
137            state: CompressState::Reading,
138            pending_trailers: None,
139        }
140    }
141
142    /// Polls the inner body and compresses data.
143    fn poll_compressed<B>(
144        &mut self,
145        cx: &mut Context<'_>,
146        mut inner: Pin<&mut B>,
147    ) -> Poll<Option<Result<Frame<Bytes>, io::Error>>>
148    where
149        B: StreamingBody,
150        B::Data: Buf,
151        B::Error: Into<BoxError>,
152    {
153        loop {
154            match self.state {
155                CompressState::Done => return Poll::Ready(None),
156
157                CompressState::Trailers => {
158                    if let Some(trailers) = self.pending_trailers.take() {
159                        self.state = CompressState::Done;
160                        return Poll::Ready(Some(Ok(Frame::trailers(trailers))));
161                    } else {
162                        self.state = CompressState::Done;
163                        return Poll::Ready(None);
164                    }
165                }
166
167                CompressState::Finishing => {
168                    self.output_buffer.reserve(Self::INTERNAL_BUF_CAPACITY);
169                    let mut output = util::WriteBuffer::new_uninitialized(
170                        self.output_buffer.spare_capacity_mut(),
171                    );
172
173                    match self.encoder.finish(&mut output) {
174                        Ok(done) => {
175                            let written = output.written_len();
176                            // Commit the bytes written to spare capacity
177                            unsafe {
178                                self.output_buffer
179                                    .set_len(self.output_buffer.len() + written);
180                            }
181
182                            if written > 0 {
183                                let data = self.output_buffer.split().freeze();
184                                if done {
185                                    self.state = if self.pending_trailers.is_some() {
186                                        CompressState::Trailers
187                                    } else {
188                                        CompressState::Done
189                                    };
190                                }
191                                return Poll::Ready(Some(Ok(Frame::data(data))));
192                            } else if done {
193                                self.state = if self.pending_trailers.is_some() {
194                                    CompressState::Trailers
195                                } else {
196                                    CompressState::Done
197                                };
198                            } else {
199                                // If not done and nothing written, we must yield to avoid busy-looping
200                                // though finish() usually finishes or writes.
201                                return Poll::Pending;
202                            }
203                        }
204                        Err(e) => return Poll::Ready(Some(Err(io::Error::other(e)))),
205                    }
206                }
207
208                CompressState::Reading => {
209                    match inner.as_mut().poll_frame(cx) {
210                        Poll::Pending => return Poll::Pending,
211                        Poll::Ready(None) => {
212                            self.state = CompressState::Finishing;
213                        }
214                        Poll::Ready(Some(Err(e))) => {
215                            return Poll::Ready(Some(Err(io::Error::other(e.into()))));
216                        }
217                        Poll::Ready(Some(Ok(frame))) => {
218                            match frame.into_data() {
219                                Ok(mut data) => {
220                                    let input_bytes = data.copy_to_bytes(data.remaining());
221                                    match self.compress_chunk(&input_bytes) {
222                                        Ok(Some(frame)) => return Poll::Ready(Some(Ok(frame))),
223                                        // Encoder buffered the data but didn't emit a block yet.
224                                        // We MUST continue to poll the inner body for more data.
225                                        Ok(None) => (),
226                                        Err(e) => return Poll::Ready(Some(Err(e))),
227                                    }
228                                }
229                                Err(frame) => {
230                                    if let Ok(trailers) = frame.into_trailers() {
231                                        self.pending_trailers = Some(trailers);
232                                        self.state = CompressState::Finishing;
233                                    }
234                                }
235                            }
236                        }
237                    }
238                }
239            }
240        }
241    }
242
243    /// Compresses a chunk of input data.
244    fn compress_chunk(&mut self, input: &[u8]) -> io::Result<Option<Frame<Bytes>>> {
245        let mut input_buf = util::PartialBuffer::new(input);
246
247        loop {
248            self.output_buffer.reserve(Self::INTERNAL_BUF_CAPACITY);
249            let mut output =
250                util::WriteBuffer::new_uninitialized(self.output_buffer.spare_capacity_mut());
251
252            self.encoder.encode(&mut input_buf, &mut output)?;
253
254            let written = output.written_len();
255            unsafe {
256                self.output_buffer
257                    .set_len(self.output_buffer.len() + written);
258            }
259
260            if input_buf.written_len() >= input.len() || written == 0 {
261                break;
262            }
263        }
264
265        if self.always_flush {
266            self.output_buffer.reserve(Self::INTERNAL_BUF_CAPACITY);
267            let mut output =
268                util::WriteBuffer::new_uninitialized(self.output_buffer.spare_capacity_mut());
269            _ = self.encoder.flush(&mut output)?;
270            let written = output.written_len();
271            unsafe {
272                self.output_buffer
273                    .set_len(self.output_buffer.len() + written);
274            }
275        }
276
277        if self.output_buffer.is_empty() {
278            Ok(None)
279        } else {
280            Ok(Some(Frame::data(self.output_buffer.split().freeze())))
281        }
282    }
283}
284
285impl<B> StreamCompressionBody<B>
286where
287    B: StreamingBody,
288{
289    #[inline(always)]
290    pub(super) fn gzip(inner: B, level: CompressionLevel, always_flush: bool) -> Self {
291        Self::compressed(
292            inner,
293            Encoder::Gzip(GzipEncoder::new(level.into_compression_core().into())),
294            always_flush,
295        )
296    }
297
298    #[inline(always)]
299    pub(super) fn deflate(inner: B, level: CompressionLevel, always_flush: bool) -> Self {
300        Self::compressed(
301            inner,
302            Encoder::Deflate(ZlibEncoder::new(level.into_compression_core().into())),
303            always_flush,
304        )
305    }
306
307    #[inline(always)]
308    pub(super) fn brotli(inner: B, level: CompressionLevel, always_flush: bool) -> Self {
309        let params = BrotliEncoderParams::default().quality(level.into_compression_core());
310        Self::compressed(
311            inner,
312            Encoder::Brotli(Box::new(BrotliEncoder::new(params))),
313            always_flush,
314        )
315    }
316
317    #[inline(always)]
318    pub(super) fn zstd(inner: B, level: CompressionLevel, always_flush: bool) -> Self {
319        // See https://issues.chromium.org/issues/41493659:
320        //  "For memory usage reasons, Chromium limits the window size to 8MB"
321        // See https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor
322        //  "For improved interoperability, it's recommended for decoders to support values
323        //  of Window_Size up to 8 MB and for encoders not to generate frames requiring a
324        //  Window_Size larger than 8 MB."
325        // Level 17 in zstd (as of v1.5.6) is the first level with a window size of 8 MB (2^23):
326        // https://github.com/facebook/zstd/blob/v1.5.6/lib/compress/clevels.h#L25-L51
327        // Set the parameter for all levels >= 17. This will either have no effect (but reduce
328        // the risk of future changes in zstd) or limit the window log to 8MB.
329        let needs_window_limit = match level {
330            CompressionLevel::Best => true, // level 20
331            CompressionLevel::Precise(level) => level >= 17,
332            CompressionLevel::Default | CompressionLevel::Fastest => false,
333        };
334
335        let level = match level {
336            CompressionLevel::Fastest => 1,
337            CompressionLevel::Best => 21,
338            CompressionLevel::Default => 0,
339            CompressionLevel::Precise(level) => level as i32,
340        };
341
342        // The parameter is not set for levels below 17 as it will increase the window size
343        // for those levels.
344        let encoder = if needs_window_limit {
345            let params = [compression_codecs::zstd::params::CParameter::window_log(23)];
346            ZstdEncoder::new_with_params(level, &params)
347        } else {
348            ZstdEncoder::new(level)
349        };
350
351        Self::compressed(inner, Encoder::Zstd(encoder), always_flush)
352    }
353
354    fn compressed(inner: B, encoder: Encoder, always_flush: bool) -> Self {
355        Self {
356            inner: BodyInner::Compress {
357                inner,
358                data: CompressData::new(encoder, always_flush),
359            },
360        }
361    }
362
363    pub(super) fn identity(inner: B) -> Self {
364        Self {
365            inner: BodyInner::Identity { inner },
366        }
367    }
368}
369
370impl<B> StreamingBody for StreamCompressionBody<B>
371where
372    B: StreamingBody,
373    B::Data: Buf,
374    B::Error: Into<BoxError>,
375{
376    type Data = Bytes;
377    type Error = io::Error;
378
379    fn poll_frame(
380        self: Pin<&mut Self>,
381        cx: &mut Context<'_>,
382    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
383        match self.project().inner.project() {
384            BodyInnerProj::Identity { inner } => {
385                // Pass through frames, converting data to Bytes
386                match inner.poll_frame(cx) {
387                    Poll::Pending => Poll::Pending,
388                    Poll::Ready(None) => Poll::Ready(None),
389                    Poll::Ready(Some(Ok(frame))) => {
390                        let frame = frame.map_data(|mut data| data.copy_to_bytes(data.remaining()));
391                        Poll::Ready(Some(Ok(frame)))
392                    }
393                    Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(io::Error::other(e.into())))),
394                }
395            }
396            BodyInnerProj::Compress { inner, data } => data.poll_compressed(cx, inner),
397        }
398    }
399
400    fn is_end_stream(&self) -> bool {
401        match &self.inner {
402            BodyInner::Identity { inner } => inner.is_end_stream(),
403            BodyInner::Compress { data, .. } => data.state == CompressState::Done,
404        }
405    }
406
407    fn size_hint(&self) -> SizeHint {
408        match &self.inner {
409            BodyInner::Identity { inner } => inner.size_hint(),
410            // Compressed size is unknown
411            BodyInner::Compress { .. } => SizeHint::default(),
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use std::collections::VecDeque;
420
421    /// A test body that yields predefined frames.
422    struct TestBody {
423        frames: VecDeque<Frame<Bytes>>,
424    }
425
426    impl TestBody {
427        fn new(frames: Vec<Frame<Bytes>>) -> Self {
428            Self {
429                frames: frames.into(),
430            }
431        }
432    }
433
434    impl StreamingBody for TestBody {
435        type Data = Bytes;
436        type Error = std::convert::Infallible;
437
438        fn poll_frame(
439            mut self: Pin<&mut Self>,
440            _cx: &mut Context<'_>,
441        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
442            match self.frames.pop_front() {
443                Some(frame) => Poll::Ready(Some(Ok(frame))),
444                None => Poll::Ready(None),
445            }
446        }
447    }
448
449    fn poll_body<B: StreamingBody + Unpin>(
450        body: &mut B,
451    ) -> Option<Result<Frame<B::Data>, B::Error>> {
452        let waker = std::task::Waker::noop();
453        let mut cx = Context::from_waker(waker);
454        match Pin::new(body).poll_frame(&mut cx) {
455            Poll::Ready(result) => result,
456            Poll::Pending => None,
457        }
458    }
459
460    #[test]
461    fn test_identity_data() {
462        let inner = TestBody::new(vec![Frame::data(Bytes::from("hello world"))]);
463        let mut body = StreamCompressionBody::identity(inner);
464
465        let frame = poll_body(&mut body).unwrap().unwrap();
466        assert!(frame.is_data());
467        assert_eq!(frame.into_data().unwrap(), Bytes::from("hello world"));
468
469        assert!(poll_body(&mut body).is_none());
470    }
471
472    #[test]
473    fn test_passthrough_trailers() {
474        let mut trailers = HeaderMap::new();
475        trailers.insert("x-checksum", "abc123".parse().unwrap());
476
477        let inner = TestBody::new(vec![
478            Frame::data(Bytes::from("data")),
479            Frame::trailers(trailers.clone()),
480        ]);
481        let mut body = StreamCompressionBody::identity(inner);
482
483        // First frame is data
484        let frame = poll_body(&mut body).unwrap().unwrap();
485        assert!(frame.is_data());
486
487        // Second frame is trailers
488        let frame = poll_body(&mut body).unwrap().unwrap();
489        assert!(frame.is_trailers());
490        let received_trailers = frame.into_trailers().unwrap();
491        assert_eq!(received_trailers.get("x-checksum").unwrap(), "abc123");
492
493        assert!(poll_body(&mut body).is_none());
494    }
495
496    #[test]
497    fn test_compressed_produces_output() {
498        let mk_inner = || TestBody::new(vec![Frame::data(Bytes::from("hello world"))]);
499        for mut body in [
500            StreamCompressionBody::gzip(mk_inner(), Default::default(), false),
501            StreamCompressionBody::deflate(mk_inner(), Default::default(), false),
502            StreamCompressionBody::brotli(mk_inner(), Default::default(), false),
503            StreamCompressionBody::zstd(mk_inner(), Default::default(), false),
504        ] {
505            // Should get compressed data
506            let frame = poll_body(&mut body).unwrap().unwrap();
507            assert!(frame.is_data());
508            let data = frame.into_data().unwrap();
509            // Compressed output should exist (gzip header starts with 0x1f 0x8b)
510            assert!(!data.is_empty());
511
512            // Should get more data from finishing
513            while let Some(Ok(frame)) = poll_body(&mut body) {
514                assert!(frame.is_data());
515            }
516        }
517    }
518
519    #[test]
520    fn test_compressed_with_trailers() {
521        let mk_inner = || {
522            let mut trailers = HeaderMap::new();
523            trailers.insert("x-checksum", "abc123".parse().unwrap());
524
525            TestBody::new(vec![
526                Frame::data(Bytes::from("hello world")),
527                Frame::trailers(trailers),
528            ])
529        };
530
531        for mut body in [
532            StreamCompressionBody::gzip(mk_inner(), Default::default(), false),
533            StreamCompressionBody::deflate(mk_inner(), Default::default(), false),
534            StreamCompressionBody::brotli(mk_inner(), Default::default(), false),
535            StreamCompressionBody::zstd(mk_inner(), Default::default(), false),
536        ] {
537            // Collect all frames
538            let mut data_frames = 0;
539            let mut trailer_frame = None;
540            while let Some(Ok(frame)) = poll_body(&mut body) {
541                if frame.is_data() {
542                    data_frames += 1;
543                } else if frame.is_trailers() {
544                    trailer_frame = Some(frame);
545                }
546            }
547
548            // Should have received at least one data frame
549            assert!(data_frames >= 1);
550
551            // Should have received trailers
552            let trailers = trailer_frame
553                .expect("Expected trailers frame")
554                .into_trailers()
555                .unwrap();
556            assert_eq!(trailers.get("x-checksum").unwrap(), "abc123");
557        }
558    }
559}