Skip to main content

rama_http/layer/decompression/
mod.rs

1//! Middleware that decompresses request and response bodies.
2//!
3//! # Examples
4//!
5//! #### Request
6//! ```rust
7//! use std::{error::Error, io::Write};
8//!
9//! use rama_core::bytes::{Bytes, BytesMut};
10//! use flate2::{write::GzEncoder, Compression};
11//!
12//! use rama_http::{Body, header, HeaderValue, Request, Response};
13//! use rama_core::service::service_fn;
14//! use rama_core::{Service, Layer};
15//! use rama_http::layer::decompression::{DecompressionBody, RequestDecompressionLayer};
16//! use rama_http::body::util::BodyExt;
17//! use rama_core::error::BoxError;
18//!
19//! # #[tokio::main]
20//! # async fn main() -> Result<(), BoxError> {
21//! // A request encoded with gzip coming from some HTTP client.
22//! let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
23//! encoder.write_all(b"Hello?")?;
24//! let request = Request::builder()
25//!     .header(header::CONTENT_ENCODING, "gzip")
26//!     .body(Body::from(encoder.finish()?))?;
27//!
28//! // Our HTTP server
29//! let mut server = (
30//!     // Automatically decompress request bodies.
31//!     RequestDecompressionLayer::new(),
32//! ).into_layer(service_fn(handler));
33//!
34//! // Send the request, with the gzip encoded body, to our server.
35//! let _response = server.serve(request).await?;
36//!
37//! // Handler receives request whose body is decoded when read
38//! async fn handler(mut req: Request<DecompressionBody<Body>>) -> Result<Response, BoxError>{
39//!     let data = req.into_body().collect().await?.to_bytes();
40//!     assert_eq!(&data[..], b"Hello?");
41//!     Ok(Response::new(Body::from("Hello, World!")))
42//! }
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! #### Response
48//!
49//! ```rust
50//! use std::convert::Infallible;
51//!
52//! use rama_core::bytes::{Bytes, BytesMut};
53//!
54//! use rama_http::{Body, Request, Response};
55//! use rama_core::service::service_fn;
56//! use rama_core::{Service, Layer};
57//! use rama_http::layer::{compression::Compression, decompression::DecompressionLayer};
58//! use rama_http::body::util::BodyExt;
59//! use rama_core::error::BoxError;
60//!
61//! #
62//! # #[tokio::main]
63//! # async fn main() -> Result<(), BoxError> {
64//! # async fn handle(req: Request) -> Result<Response, Infallible> {
65//! #     let body = Body::from("Hello, World!");
66//! #     Ok(Response::new(body))
67//! # }
68//!
69//! // Some opaque service that applies compression.
70//! let service = Compression::new(service_fn(handle));
71//!
72//! // Our HTTP client.
73//! let mut client = (
74//!     // Automatically decompress response bodies.
75//!     DecompressionLayer::new(),
76//! ).into_layer(service);
77//!
78//! // Call the service.
79//! //
80//! // `DecompressionLayer` takes care of setting `Accept-Encoding`.
81//! let request = Request::new(Body::default());
82//!
83//! let response = client
84//!     .serve(request)
85//!     .await?;
86//!
87//! // Read the body
88//! let body = response.into_body();
89//! let bytes = body.collect().await?.to_bytes().to_vec();
90//! let body = String::from_utf8(bytes).map_err(Into::<BoxError>::into)?;
91//!
92//! assert_eq!(body, "Hello, World!");
93//! #
94//! # Ok(())
95//! # }
96//! ```
97
98mod request;
99
100use rama_core::extensions::Extension;
101
102pub(crate) mod body;
103mod layer;
104mod service;
105
106/// Marker extension inserted into a response when [`Decompression`] unwraps a
107/// compressed response body.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Extension)]
109#[extension(tags(http))]
110pub enum DecompressedFrom {
111    Gzip,
112    Deflate,
113    Brotli,
114    Zstd,
115}
116
117#[doc(inline)]
118pub use self::{
119    body::DecompressionBody,
120    layer::DecompressionLayer,
121    service::{Decompression, DefaultDecompressionMatcher},
122};
123
124#[doc(inline)]
125pub use self::request::layer::RequestDecompressionLayer;
126#[doc(inline)]
127pub use self::request::service::RequestDecompression;
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    use std::convert::Infallible;
134    use std::io::Write;
135    use std::time::Duration;
136
137    use crate::layer::compression::Compression;
138    use crate::{Body, HeaderMap, HeaderName, Request, Response, body::util::BodyExt};
139    use rama_core::error::ErrorContext;
140    use rama_core::extensions::ExtensionsRef;
141    use rama_core::futures::{StreamExt as _, stream};
142    use rama_core::matcher::service::MatcherServicePair;
143    use rama_core::service::service_fn;
144    use rama_core::{Service, bytes::Bytes};
145
146    use rama_http_types::{BodyExtractExt, header};
147
148    #[tokio::test]
149    async fn works() {
150        let client = Decompression::new(Compression::new(service_fn(handle)));
151
152        let req = Request::builder()
153            .header("accept-encoding", "gzip")
154            .body(Body::empty())
155            .unwrap();
156        let res = client.serve(req).await.unwrap();
157
158        assert_eq!(
159            res.extensions().get_ref::<DecompressedFrom>(),
160            Some(&DecompressedFrom::Gzip)
161        );
162
163        // read the body, it will be decompressed automatically
164        let body = res.into_body();
165        let collected = body.collect().await.unwrap();
166        let decompressed_data = String::from_utf8(collected.to_bytes().to_vec()).unwrap();
167
168        assert_eq!(
169            decompressed_data,
170            "Hello, World! Hello, World! Hello, World!"
171        );
172    }
173
174    async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
175        let mut trailers = HeaderMap::new();
176        trailers.insert(HeaderName::from_static("foo"), "bar".parse().unwrap());
177        let body = Body::from("Hello, World! Hello, World! Hello, World!");
178        Ok(Response::builder().body(body).unwrap())
179    }
180
181    #[tokio::test]
182    async fn decompress_multi_zstd() {
183        let client = Decompression::new(service_fn(handle_multi_zstd));
184
185        let req = Request::builder()
186            .header("accept-encoding", "zstd")
187            .body(Body::empty())
188            .unwrap();
189        let res = client.serve(req).await.unwrap();
190
191        // read the body, it will be decompressed automatically
192        let body = res.into_body();
193        let decompressed_data =
194            String::from_utf8(body.collect().await.unwrap().to_bytes().to_vec()).unwrap();
195
196        assert_eq!(decompressed_data, "Hello, World!");
197    }
198
199    async fn handle_multi_zstd(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
200        let mut buf = Vec::new();
201        let mut enc1 = zstd::Encoder::new(&mut buf, Default::default()).unwrap();
202        enc1.write_all(b"Hello, ").unwrap();
203        enc1.finish().unwrap();
204
205        let mut enc2 = zstd::Encoder::new(&mut buf, Default::default()).unwrap();
206        enc2.write_all(b"World!").unwrap();
207        enc2.finish().unwrap();
208
209        let mut res = Response::new(Body::from(buf));
210        res.headers_mut()
211            .insert("content-encoding", "zstd".parse().unwrap());
212        Ok(res)
213    }
214
215    #[tokio::test]
216    async fn decompress_empty() {
217        let client = Decompression::new(Compression::new(service_fn(handle_empty)));
218
219        let req = Request::builder()
220            .header("accept-encoding", "gzip")
221            .body(Body::empty())
222            .unwrap();
223        let res = client.serve(req).await.unwrap();
224
225        let decompressed_data = res.try_into_string().await.unwrap();
226
227        assert_eq!(decompressed_data, "");
228    }
229
230    async fn handle_empty(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
231        let mut res = Response::new(Body::empty());
232        res.headers_mut()
233            .insert("content-encoding", "gzip".parse().unwrap());
234        Ok(res)
235    }
236
237    #[tokio::test]
238    async fn decompress_empty_with_trailers() {
239        let client = Decompression::new(Compression::new(service_fn(handle_empty_with_trailers)));
240        let req = Request::builder()
241            .header("accept-encoding", "gzip")
242            .body(Body::empty())
243            .unwrap();
244        let res = client.serve(req).await.unwrap();
245        let body = res.into_body();
246        let collected = body.collect().await.unwrap();
247        let trailers = collected.trailers().cloned().unwrap(); // TODO
248        let decompressed_data = String::from_utf8(collected.to_bytes().to_vec()).unwrap();
249        assert_eq!(decompressed_data, "");
250        assert_eq!(trailers["foo"], "bar");
251    }
252
253    async fn handle_empty_with_trailers(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
254        let mut trailers = HeaderMap::new();
255        trailers.insert(HeaderName::from_static("foo"), "bar".parse().unwrap());
256        let body = Body::empty().with_trailer_headers(trailers);
257        Ok(Response::builder()
258            .header("content-encoding", "gzip")
259            .body(body)
260            .unwrap())
261    }
262
263    #[tokio::test]
264    async fn does_not_insert_accept_encoding_when_disabled() {
265        let client = Decompression::new(service_fn(|req: Request<Body>| async move {
266            assert!(!req.headers().contains_key(header::ACCEPT_ENCODING));
267            Ok::<_, Infallible>(Response::new(Body::empty()))
268        }))
269        .with_insert_accept_encoding_header(false);
270
271        let req = Request::new(Body::empty());
272        _ = client.serve(req).await.unwrap();
273    }
274
275    // A long, mildly-varied plaintext so the brotli stream spans multiple chunks
276    // (guarantees the decoder yields some output before the truncation errors).
277    fn long_plaintext() -> String {
278        (0..4000)
279            .map(|i| format!("line {i}: the quick brown fox\n"))
280            .collect()
281    }
282
283    // Corrupt a run of bytes in the middle of a valid brotli stream: the decoder
284    // produces output for the valid prefix, then hits an invalid block and errors
285    // with InvalidData (the real "brotli error" case — a clean tail-truncation
286    // instead hits brotli's UnexpectedEof clean-EOF path and would NOT error).
287    fn corrupt_brotli(plaintext: &str) -> Vec<u8> {
288        let mut full = Vec::new();
289        {
290            let mut w = brotli::CompressorWriter::new(&mut full, 4096, 5, 22);
291            w.write_all(plaintext.as_bytes()).unwrap();
292        } // drop finishes the stream
293        let n = full.len();
294        let start = n / 3;
295        let end = (start + n / 4).min(n);
296        for b in &mut full[start..end] {
297            *b ^= 0xFF;
298        }
299        full
300    }
301
302    async fn collect_truncated_br(
303        tolerate: bool,
304    ) -> Result<rama_core::bytes::Bytes, rama_core::error::BoxError> {
305        let body = corrupt_brotli(&long_plaintext());
306        let client = Decompression::new(service_fn(move |_req: Request<Body>| {
307            let body = body.clone();
308            async move {
309                Ok::<_, Infallible>(
310                    Response::builder()
311                        .header(header::CONTENT_ENCODING, "br")
312                        .body(Body::from(body))
313                        .unwrap(),
314                )
315            }
316        }))
317        .with_tolerate_decode_errors(tolerate);
318        let req = Request::builder()
319            .header("accept-encoding", "br")
320            .body(Body::empty())
321            .unwrap();
322        let res = client.serve(req).await.unwrap();
323        res.into_body()
324            .collect()
325            .await
326            .map(|c| c.to_bytes())
327            .into_box_error()
328    }
329
330    #[tokio::test]
331    async fn truncated_brotli_with_tolerance_ends_cleanly() {
332        let collected = collect_truncated_br(true).await;
333        assert!(
334            collected.is_ok(),
335            "tolerant decode must end the stream cleanly, not error"
336        );
337        let bytes = collected.unwrap();
338        // The salvaged output must be a clean prefix of the original (no garbage
339        // from the corrupt block); it may be empty if the decoder had not flushed
340        // output before the corruption — the load-bearing property is that the
341        // stream ENDS rather than ABORTS.
342        assert!(
343            long_plaintext().as_bytes().starts_with(&bytes),
344            "decoded bytes must be a clean prefix of the original plaintext (len={})",
345            bytes.len()
346        );
347    }
348
349    #[tokio::test]
350    async fn truncated_brotli_without_tolerance_still_errors() {
351        // Regression guard: the default (strict) behavior must be unchanged.
352        assert!(
353            collect_truncated_br(false).await.is_err(),
354            "strict decode must surface the truncation as an error"
355        );
356    }
357
358    #[tokio::test]
359    async fn response_matcher_can_disable_decompression() {
360        let client = Decompression::new(Compression::new(service_fn(handle)))
361            .with_matcher(MatcherServicePair(true, MatcherServicePair(false, ())));
362
363        let req = Request::builder()
364            .header("accept-encoding", "gzip")
365            .body(Body::empty())
366            .unwrap();
367        let res = client.serve(req).await.unwrap();
368
369        assert!(res.extensions().get_ref::<DecompressedFrom>().is_none());
370        assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
371
372        let compressed = res.into_body().collect().await.unwrap().to_bytes();
373        assert_ne!(
374            std::str::from_utf8(&compressed).unwrap_or_default(),
375            "Hello, World! Hello, World! Hello, World!"
376        );
377    }
378
379    #[tokio::test]
380    async fn brotli_rejects_extra_data_without_waiting_for_end_of_body() {
381        let mut compressed = Vec::new();
382        {
383            let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 5, 20);
384            encoder.write_all(b"Hello, World!").unwrap();
385        }
386
387        let svc = service_fn(move |_req: Request<Body>| {
388            let compressed = compressed.clone();
389            async move {
390                let stream = stream::iter([
391                    Ok::<_, Infallible>(Bytes::from(compressed)),
392                    Ok(Bytes::from_static(b"extra")),
393                ])
394                .chain(stream::pending());
395
396                Ok::<_, Infallible>(
397                    Response::builder()
398                        .header("content-encoding", "br")
399                        .body(Body::from_stream(stream))
400                        .unwrap(),
401                )
402            }
403        });
404        let client = Decompression::new(svc);
405
406        let res = client.serve(Request::new(Body::empty())).await.unwrap();
407
408        let result = tokio::time::timeout(Duration::from_secs(1), res.into_body().collect())
409            .await
410            .expect("extra data should produce an error without waiting for the body to end");
411        _ = result.unwrap_err();
412    }
413
414    #[tokio::test]
415    async fn brotli_keeps_trailers_that_follow_an_empty_data_frame() {
416        let mut compressed = Vec::new();
417        {
418            let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 5, 20);
419            encoder.write_all(b"Hello, World!").unwrap();
420        }
421
422        let svc = service_fn(move |_req: Request<Body>| {
423            let compressed = compressed.clone();
424            async move {
425                // An empty data frame between the payload and trailers is
426                // legal and must not be read as the end of the body.
427                let stream = stream::iter([
428                    Ok::<_, Infallible>(Bytes::from(compressed)),
429                    Ok(Bytes::new()),
430                ]);
431                let mut trailers = HeaderMap::new();
432                trailers.insert(HeaderName::from_static("foo"), "bar".parse().unwrap());
433
434                Ok::<_, Infallible>(
435                    Response::builder()
436                        .header("content-encoding", "br")
437                        .body(Body::from_stream(stream).with_trailer_headers(trailers))
438                        .unwrap(),
439                )
440            }
441        });
442        let client = Decompression::new(svc);
443
444        let res = client.serve(Request::new(Body::empty())).await.unwrap();
445        let collected = res.into_body().collect().await.unwrap();
446        let trailers = collected
447            .trailers()
448            .cloned()
449            .expect("trailers following an empty data frame should still arrive");
450
451        assert_eq!(trailers["foo"], "bar");
452        assert_eq!(
453            String::from_utf8(collected.to_bytes().to_vec()).unwrap(),
454            "Hello, World!"
455        );
456    }
457}