Skip to main content

rama_http/layer/compression/
layer.rs

1use super::predicate::DefaultPredicate;
2use super::{Compression, Predicate};
3use crate::headers::encoding::AcceptEncoding;
4use crate::layer::util::compression::CompressionLevel;
5use rama_core::Layer;
6
7/// Compress response bodies of the underlying service.
8///
9/// This uses the `Accept-Encoding` header to pick an appropriate encoding and adds the
10/// `Content-Encoding` header to responses.
11///
12/// See the [module docs](crate::layer::compression) for more details.
13#[derive(Clone, Debug)]
14pub struct CompressionLayer<P = DefaultPredicate> {
15    accept: AcceptEncoding,
16    predicate: P,
17    respect_content_encoding_if_possible: bool,
18    quality: CompressionLevel,
19    enforce_not_acceptable: bool,
20}
21
22impl<P: Default> Default for CompressionLayer<P> {
23    fn default() -> Self {
24        Self {
25            accept: AcceptEncoding::default(),
26            predicate: P::default(),
27            respect_content_encoding_if_possible: false,
28            quality: CompressionLevel::default(),
29            enforce_not_acceptable: true,
30        }
31    }
32}
33
34impl<S, P> Layer<S> for CompressionLayer<P>
35where
36    P: Predicate,
37{
38    type Service = Compression<S, P>;
39
40    fn layer(&self, inner: S) -> Self::Service {
41        Compression {
42            inner,
43            accept: self.accept,
44            predicate: self.predicate.clone(),
45            respect_content_encoding_if_possible: self.respect_content_encoding_if_possible,
46            quality: self.quality,
47            enforce_not_acceptable: self.enforce_not_acceptable,
48        }
49    }
50
51    fn into_layer(self, inner: S) -> Self::Service {
52        Compression {
53            inner,
54            accept: self.accept,
55            predicate: self.predicate,
56            respect_content_encoding_if_possible: self.respect_content_encoding_if_possible,
57            quality: self.quality,
58            enforce_not_acceptable: self.enforce_not_acceptable,
59        }
60    }
61}
62
63impl CompressionLayer {
64    /// Creates a new [`CompressionLayer`].
65    #[must_use]
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Replace the current compression predicate.
71    pub fn with_compress_predicate<C>(self, predicate: C) -> CompressionLayer<C>
72    where
73        C: Predicate,
74    {
75        CompressionLayer {
76            accept: self.accept,
77            predicate,
78            respect_content_encoding_if_possible: self.respect_content_encoding_if_possible,
79            quality: self.quality,
80            enforce_not_acceptable: self.enforce_not_acceptable,
81        }
82    }
83}
84
85impl<P> CompressionLayer<P> {
86    rama_utils::macros::generate_set_and_with! {
87        /// Sets whether to enable the gzip encoding.
88        pub fn gzip(mut self, enable: bool) -> Self {
89            self.accept.set_gzip(enable);
90            self
91        }
92    }
93
94    rama_utils::macros::generate_set_and_with! {
95        /// Sets whether to enable the Deflate encoding.
96        pub fn deflate(mut self, enable: bool) -> Self {
97            self.accept.set_deflate(enable);
98            self
99        }
100    }
101
102    rama_utils::macros::generate_set_and_with! {
103        /// Sets whether to enable the Brotli encoding.
104        pub fn br(mut self, enable: bool) -> Self {
105            self.accept.set_br(enable);
106            self
107        }
108    }
109
110    rama_utils::macros::generate_set_and_with! {
111        /// Sets whether to enable the Zstd encoding.
112        pub fn zstd(mut self, enable: bool) -> Self {
113            self.accept.set_zstd(enable);
114            self
115        }
116    }
117
118    rama_utils::macros::generate_set_and_with! {
119        /// Sets the compression quality.
120        pub fn quality(mut self, quality: CompressionLevel) -> Self {
121            self.quality = quality;
122            self
123        }
124    }
125
126    rama_utils::macros::generate_set_and_with! {
127        /// Allow responses with content-encoding.
128        ///
129        /// Useful in case your stack uses that response header as preference.
130        /// Not something you want for regular servers or proxies however,
131        /// or most use cases for that matter.
132        pub fn respect_content_encoding_if_possible(mut self) -> Self {
133            self.respect_content_encoding_if_possible = true;
134            self
135        }
136    }
137
138    rama_utils::macros::generate_set_and_with! {
139        /// Sets whether to respond with `406 Not Acceptable` when the client's
140        /// `Accept-Encoding` header rejects every available representation
141        /// (e.g. `*;q=0` or a lone `identity;q=0`), as recommended by RFC 9110 ยง12.5.3.
142        ///
143        /// Enabled by default. Disable to opt out and instead fall back to sending an
144        /// uncompressed (identity) response regardless of the client's stated preference.
145        pub fn enforce_not_acceptable(mut self, enable: bool) -> Self {
146            self.enforce_not_acceptable = enable;
147            self
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    use crate::{Request, Response, body::util::BodyExt, header::ACCEPT_ENCODING};
157    use rama_core::Service;
158    use rama_core::service::service_fn;
159    use rama_core::stream::io::ReaderStream;
160    use rama_http_types::Body;
161    use std::convert::Infallible;
162    use tokio::fs::File;
163
164    async fn handle(_req: Request) -> Result<Response, Infallible> {
165        // Open the file.
166        let file = File::open("Cargo.toml").await.expect("file missing");
167        // Convert the file into a `Stream`.
168        let stream = ReaderStream::new(file);
169        // Convert the `Stream` into a `Body`.
170        let body = Body::from_stream(stream);
171        // Create response.
172        Ok(Response::new(body))
173    }
174
175    #[tokio::test]
176    async fn accept_encoding_configuration_works() -> Result<(), rama_core::error::BoxError> {
177        use std::io::Read;
178
179        fn decode<R: Read>(mut r: R) -> std::io::Result<Vec<u8>> {
180            let mut buf = Vec::new();
181            r.read_to_end(&mut buf)?;
182            Ok(buf)
183        }
184
185        // Read the source file once so we can verify each response round-trips to the same bytes.
186        let expected = tokio::fs::read("Cargo.toml").await?;
187
188        // Configure a layer that only offers deflate, then confirm the response is actually
189        // deflate-encoded by decoding it and comparing to the original content.
190
191        let deflate_only_layer = CompressionLayer::new()
192            .with_quality(CompressionLevel::Best)
193            .with_br(false)
194            .with_gzip(false);
195
196        let service = deflate_only_layer.into_layer(service_fn(handle));
197
198        let request = Request::builder()
199            .header(ACCEPT_ENCODING, "gzip, deflate, br")
200            .body(Body::empty())?;
201
202        let response = service.serve(request).await?;
203
204        assert_eq!(response.headers()["content-encoding"], "deflate");
205
206        let deflate_body = response.into_body().collect().await?.to_bytes();
207
208        // The "deflate" Content-Encoding is RFC 1950 zlib framing (2-byte header + Adler-32),
209        // not raw RFC 1951 deflate, so use ZlibDecoder rather than DeflateDecoder.
210        let decoded = decode(flate2::bufread::ZlibDecoder::new(&deflate_body[..]))?;
211        assert_eq!(decoded, expected);
212
213        // Same check for brotli.
214        let br_only_layer = CompressionLayer::new()
215            .with_quality(CompressionLevel::Best)
216            .with_gzip(false)
217            .with_deflate(false);
218
219        let service = br_only_layer.into_layer(service_fn(handle));
220
221        let request = Request::builder()
222            .header(ACCEPT_ENCODING, "gzip, deflate, br")
223            .body(Body::empty())?;
224
225        let response = service.serve(request).await?;
226
227        assert_eq!(response.headers()["content-encoding"], "br");
228
229        let br_body = response.into_body().collect().await?.to_bytes();
230
231        // 4096 is the decoder's internal read-buffer size, not a content-length bound.
232        let decoded = decode(brotli::Decompressor::new(&br_body[..], 4096))?;
233        assert_eq!(decoded, expected);
234
235        Ok(())
236    }
237
238    #[tokio::test]
239    async fn zstd_is_web_safe() -> Result<(), rama_core::error::BoxError> {
240        // Test ensuring that zstd compression will not exceed an 8MiB window size; browsers do not
241        // accept responses using 16MiB+ window sizes.
242
243        async fn zeroes(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
244            Ok(Response::new(Body::from(vec![0u8; 18_874_368])))
245        }
246        // zstd will (I believe) lower its window size if a larger one isn't beneficial and
247        // it knows the size of the input; use an 18MiB body to ensure it would want a
248        // >=16MiB window (though it might not be able to see the input size here).
249
250        let zstd_layer = CompressionLayer::new()
251            .with_quality(CompressionLevel::Best)
252            .with_br(false)
253            .with_deflate(false)
254            .with_gzip(false);
255
256        let service = zstd_layer.into_layer(service_fn(zeroes));
257
258        let request = Request::builder()
259            .header(ACCEPT_ENCODING, "zstd")
260            .body(Body::empty())?;
261
262        let response = service.serve(request).await?;
263
264        assert_eq!(response.headers()["content-encoding"], "zstd");
265
266        let body = response.into_body();
267        let bytes = body.collect().await?.to_bytes();
268        let mut dec = zstd::Decoder::new(&*bytes)?;
269        dec.window_log_max(23)?; // Limit window size accepted by decoder to 2 ^ 23 bytes (8MiB)
270
271        std::io::copy(&mut dec, &mut std::io::sink())?;
272
273        Ok(())
274    }
275}