Skip to main content

rust_webx_host/
compression.rs

1//! Response compression using gzip/deflate (via flate2).
2//!
3//! Compression is applied at the hyper response layer.
4//! This module provides the `compress_body` helper function
5//! and a middleware that sets the Vary header.
6
7use flate2::write::GzEncoder;
8use flate2::Compression;
9use rust_webx_core::error::Result;
10use rust_webx_core::http::IHttpContext;
11use rust_webx_core::middleware::IMiddleware;
12use std::io::Write;
13use std::ops::ControlFlow;
14
15/// Compress a byte buffer using gzip at the given compression level (0-9).
16pub fn compress_gzip(data: &[u8], level: u32) -> Option<Vec<u8>> {
17    let mut encoder = GzEncoder::new(Vec::with_capacity(data.len() / 2), Compression::new(level));
18    encoder.write_all(data).ok()?;
19    encoder.finish().ok()
20}
21
22/// Compression configuration.
23pub struct CompressionConfig {
24    pub level: u32,
25    pub min_size: usize,
26}
27
28impl Default for CompressionConfig {
29    fn default() -> Self {
30        Self {
31            level: 6,
32            min_size: 1024,
33        }
34    }
35}
36
37impl CompressionConfig {
38    pub fn new() -> Self {
39        Self::default()
40    }
41    pub fn level(mut self, l: u32) -> Self {
42        self.level = l;
43        self
44    }
45    pub fn min_size(mut self, s: usize) -> Self {
46        self.min_size = s;
47        self
48    }
49}
50
51/// Middleware that compresses response bodies with gzip when the client
52/// supports it (via `Accept-Encoding: gzip`) and the response exceeds
53/// `min_size` bytes.
54///
55/// Skips already-compressed content types (images, video, audio).
56///
57/// ```ignore
58/// Host::builder()
59///     .use_middleware::<CompressionMiddleware>()
60///     .build()
61/// ```
62#[derive(Default)]
63pub struct CompressionMiddleware {
64    config: CompressionConfig,
65}
66
67impl CompressionMiddleware {
68    pub fn new() -> Self {
69        Self::default()
70    }
71    pub fn with_config(config: CompressionConfig) -> Self {
72        Self { config }
73    }
74}
75
76#[async_trait::async_trait]
77impl IMiddleware for CompressionMiddleware {
78    async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
79        ctx.response_mut().set_header("vary", "accept-encoding");
80        Ok(ControlFlow::Continue(()))
81    }
82
83    async fn after(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
84        let accept = ctx.request().header("accept-encoding").unwrap_or("");
85        if !accept.to_lowercase().contains("gzip") {
86            return Ok(());
87        }
88
89        let body = ctx.response().body_bytes();
90        if body.len() < self.config.min_size {
91            return Ok(());
92        }
93
94        let ct = ctx
95            .response()
96            .header("content-type")
97            .unwrap_or("")
98            .to_lowercase();
99        if ct.starts_with("image/") || ct.starts_with("video/") || ct.starts_with("audio/") {
100            return Ok(());
101        }
102
103        if let Some(compressed) = compress_gzip(&body, self.config.level) {
104            ctx.response_mut().set_header("content-encoding", "gzip");
105            ctx.response_mut().write_bytes(compressed).await?;
106        }
107        Ok(())
108    }
109}