rama_http/layer/decompression/
mod.rs1mod request;
99
100use rama_core::extensions::Extension;
101
102pub(crate) mod body;
103mod layer;
104mod service;
105
106#[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 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 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(); 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 fn long_plaintext() -> String {
278 (0..4000)
279 .map(|i| format!("line {i}: the quick brown fox\n"))
280 .collect()
281 }
282
283 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 } 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 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 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 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}