Skip to main content

rama_http/layer/compression/
body.rs

1#![allow(unused_imports)]
2
3use crate::HeaderMap;
4use crate::layer::util::compression::{
5    AsyncReadBody, BodyIntoStream, CompressionLevel, DecorateAsyncRead, WrapBody,
6    compressed_body_poll_frame, impl_decorate_async_read,
7};
8use rama_core::{
9    bytes::{Buf, Bytes},
10    error::BoxError,
11};
12
13use async_compression::tokio::bufread::{BrotliEncoder, GzipEncoder, ZlibEncoder, ZstdEncoder};
14use pin_project_lite::pin_project;
15use rama_core::futures::ready;
16use rama_core::stream::io::StreamReader;
17use rama_http_types::StreamingBody;
18use rama_http_types::body::Frame;
19use std::{
20    io,
21    marker::PhantomData,
22    pin::Pin,
23    task::{Context, Poll},
24};
25
26use super::pin_project_cfg::pin_project_cfg;
27
28pin_project! {
29    /// Response body of [`Compression`].
30    ///
31    /// [`Compression`]: super::Compression
32    pub struct CompressionBody<B>
33    where
34        B: StreamingBody,
35    {
36        #[pin]
37        pub(crate) inner: BodyInner<B>,
38    }
39}
40
41impl<B> Default for CompressionBody<B>
42where
43    B: StreamingBody + Default,
44{
45    fn default() -> Self {
46        Self {
47            inner: BodyInner::Identity {
48                inner: B::default(),
49            },
50        }
51    }
52}
53
54impl<B> CompressionBody<B>
55where
56    B: StreamingBody,
57{
58    pub(crate) fn new(inner: BodyInner<B>) -> Self {
59        Self { inner }
60    }
61}
62
63type GzipBody<B> = WrapBody<GzipEncoder<B>>;
64
65type DeflateBody<B> = WrapBody<ZlibEncoder<B>>;
66
67type BrotliBody<B> = WrapBody<BrotliEncoder<B>>;
68
69type ZstdBody<B> = WrapBody<ZstdEncoder<B>>;
70
71pin_project_cfg! {
72    #[project = BodyInnerProj]
73    pub(crate) enum BodyInner<B>
74    where
75        B: StreamingBody,
76    {
77        Gzip {
78            #[pin]
79            inner: GzipBody<B>,
80        },
81        Deflate {
82            #[pin]
83            inner: DeflateBody<B>,
84        },
85        Brotli {
86            #[pin]
87            inner: BrotliBody<B>,
88        },
89        Zstd {
90            #[pin]
91            inner: ZstdBody<B>,
92        },
93        Identity {
94            #[pin]
95            inner: B,
96        },
97    }
98}
99
100impl<B: StreamingBody> BodyInner<B> {
101    pub(crate) fn gzip(inner: WrapBody<GzipEncoder<B>>) -> Self {
102        Self::Gzip { inner }
103    }
104
105    pub(crate) fn deflate(inner: WrapBody<ZlibEncoder<B>>) -> Self {
106        Self::Deflate { inner }
107    }
108
109    pub(crate) fn brotli(inner: WrapBody<BrotliEncoder<B>>) -> Self {
110        Self::Brotli { inner }
111    }
112
113    pub(crate) fn zstd(inner: WrapBody<ZstdEncoder<B>>) -> Self {
114        Self::Zstd { inner }
115    }
116
117    pub(crate) fn identity(inner: B) -> Self {
118        Self::Identity { inner }
119    }
120}
121
122impl<B> StreamingBody for CompressionBody<B>
123where
124    B: StreamingBody<Error: Into<BoxError>>,
125{
126    type Data = Bytes;
127    type Error = BoxError;
128
129    fn poll_frame(
130        self: Pin<&mut Self>,
131        cx: &mut Context<'_>,
132    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
133        compressed_body_poll_frame!(self, cx)
134    }
135
136    fn size_hint(&self) -> rama_http_types::body::SizeHint {
137        if let BodyInner::Identity { inner } = &self.inner {
138            inner.size_hint()
139        } else {
140            rama_http_types::body::SizeHint::new()
141        }
142    }
143
144    fn is_end_stream(&self) -> bool {
145        if let BodyInner::Identity { inner } = &self.inner {
146            inner.is_end_stream()
147        } else {
148            false
149        }
150    }
151}
152
153impl_decorate_async_read!(GzipEncoder: |input, quality| {
154    GzipEncoder::with_quality(input, quality.into_async_compression())
155});
156
157impl_decorate_async_read!(ZlibEncoder: |input, quality| {
158    ZlibEncoder::with_quality(input, quality.into_async_compression())
159});
160
161impl_decorate_async_read!(BrotliEncoder: |input, quality| {
162    // The brotli crate used under the hood here has a default compression level of 11,
163    // which is the max for brotli. This causes extremely slow compression times, so we
164    // manually set a default of 4 here.
165    //
166    // This is the same default used by NGINX for on-the-fly brotli compression.
167    let level = match quality {
168        CompressionLevel::Default => async_compression::Level::Precise(4),
169        other => other.into_async_compression(),
170    };
171    BrotliEncoder::with_quality(input, level)
172});
173
174impl_decorate_async_read!(ZstdEncoder: |input, quality| {
175    // See https://issues.chromium.org/issues/41493659:
176    //  "For memory usage reasons, Chromium limits the window size to 8MB"
177    // See https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor
178    //  "For improved interoperability, it's recommended for decoders to support values
179    //  of Window_Size up to 8 MB and for encoders not to generate frames requiring a
180    //  Window_Size larger than 8 MB."
181    // Level 17 in zstd (as of v1.5.6) is the first level with a window size of 8 MB (2^23):
182    // https://github.com/facebook/zstd/blob/v1.5.6/lib/compress/clevels.h#L25-L51
183    // Set the parameter for all levels >= 17. This will either have no effect (but reduce
184    // the risk of future changes in zstd) or limit the window log to 8MB.
185    let needs_window_limit = match quality {
186        CompressionLevel::Best => true, // level 20
187        CompressionLevel::Precise(level) => level >= 17,
188        CompressionLevel::Default | CompressionLevel::Fastest => false,
189    };
190    // The parameter is not set for levels below 17 as it will increase the window size
191    // for those levels.
192    if needs_window_limit {
193        let params = [async_compression::zstd::CParameter::window_log(23)];
194        ZstdEncoder::with_quality_and_params(input, quality.into_async_compression(), &params)
195    } else {
196        ZstdEncoder::with_quality(input, quality.into_async_compression())
197    }
198});