Skip to main content

salvo_compression/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Compression middleware for the Salvo web framework.
4//!
5//! This middleware automatically compresses HTTP responses using various algorithms,
6//! reducing bandwidth usage and improving load times for clients.
7//!
8//! # Supported Algorithms
9//!
10//! | Algorithm | Feature | Content-Encoding |
11//! |-----------|---------|------------------|
12//! | Gzip | `gzip` | `gzip` |
13//! | Brotli | `brotli` | `br` |
14//! | Deflate | `deflate` | `deflate` |
15//! | Zstd | `zstd` | `zstd` |
16//!
17//! # Example
18//!
19//! ```ignore
20//! use salvo_compression::{Compression, CompressionLevel};
21//! use salvo_core::prelude::*;
22//!
23//! let compression = Compression::new()
24//!     .enable_gzip(CompressionLevel::Default)
25//!     .min_length(1024);  // Only compress responses > 1KB
26//!
27//! let router = Router::new()
28//!     .hoop(compression)
29//!     .get(my_handler);
30//! ```
31//!
32//! # Algorithm Negotiation
33//!
34//! The middleware negotiates the compression algorithm based on the client's
35//! `Accept-Encoding` header. By default, it respects the client's preference order.
36//! Use `force_priority(true)` to use the server's configured priority instead.
37//!
38//! # Compression Levels
39//!
40//! - [`CompressionLevel::Fastest`]: Fastest compression, larger output
41//! - [`CompressionLevel::Default`]: Balanced compression (recommended)
42//! - [`CompressionLevel::Minsize`]: Best compression, slower
43//! - `CompressionLevel::Precise(u32)`: Fine-grained control
44//!
45//! # Default Content Types
46//!
47//! By default, the middleware compresses:
48//! - `text/*` (HTML, CSS, plain text, etc.)
49//! - `application/javascript`
50//! - `application/json`
51//! - `application/xml`, `application/rss+xml`
52//! - `application/wasm`
53//! - `image/svg+xml`
54//!
55//! Use `.content_types()` to customize which MIME types are compressed.
56//!
57//! # Minimum Length
58//!
59//! Small responses may not benefit from compression. Use `.min_length(bytes)`
60//! to skip compression for responses smaller than the specified size.
61//!
62//! Read more: <https://salvo.rs>
63
64use std::fmt::{self, Display, Formatter};
65use std::str::FromStr;
66
67use indexmap::IndexMap;
68use salvo_core::http::body::ResBody;
69use salvo_core::http::header::{
70    ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue,
71};
72use salvo_core::http::{self, Mime, StatusCode, mime};
73use salvo_core::{Depot, FlowCtrl, Handler, Request, Response, async_trait};
74
75mod encoder;
76mod stream;
77use encoder::Encoder;
78use stream::EncodeStream;
79
80/// Level of compression data should be compressed with.
81#[non_exhaustive]
82#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
83pub enum CompressionLevel {
84    /// Fastest quality of compression, usually produces a bigger size.
85    Fastest,
86    /// Best quality of compression, usually produces the smallest size.
87    Minsize,
88    /// Default quality of compression defined by the selected compression algorithm.
89    #[default]
90    Default,
91    /// Precise quality based on the underlying compression algorithms'
92    /// qualities. The interpretation of this depends on the algorithm chosen
93    /// and the specific implementation backing it.
94    /// Qualities are implicitly clamped to the algorithm's maximum.
95    Precise(u32),
96}
97
98/// CompressionAlgo
99#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash)]
100#[non_exhaustive]
101pub enum CompressionAlgo {
102    /// Compress use Brotli algo.
103    #[cfg(feature = "brotli")]
104    #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
105    Brotli,
106
107    /// Compress use Deflate algo.
108    #[cfg(feature = "deflate")]
109    #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
110    Deflate,
111
112    /// Compress use Gzip algo.
113    #[cfg(feature = "gzip")]
114    #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
115    Gzip,
116
117    /// Compress use Zstd algo.
118    #[cfg(feature = "zstd")]
119    #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
120    Zstd,
121}
122
123impl FromStr for CompressionAlgo {
124    type Err = String;
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        match s {
128            #[cfg(feature = "brotli")]
129            "br" => Ok(Self::Brotli),
130            #[cfg(feature = "brotli")]
131            "brotli" => Ok(Self::Brotli),
132
133            #[cfg(feature = "deflate")]
134            "deflate" => Ok(Self::Deflate),
135
136            #[cfg(feature = "gzip")]
137            "gzip" => Ok(Self::Gzip),
138
139            #[cfg(feature = "zstd")]
140            "zstd" => Ok(Self::Zstd),
141            _ => Err(format!("unknown compression algorithm: {s}")),
142        }
143    }
144}
145
146impl Display for CompressionAlgo {
147    #[allow(unreachable_patterns)]
148    #[allow(unused_variables)]
149    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
150        match self {
151            #[cfg(feature = "brotli")]
152            Self::Brotli => write!(f, "br"),
153            #[cfg(feature = "deflate")]
154            Self::Deflate => write!(f, "deflate"),
155            #[cfg(feature = "gzip")]
156            Self::Gzip => write!(f, "gzip"),
157            #[cfg(feature = "zstd")]
158            Self::Zstd => write!(f, "zstd"),
159            _ => unreachable!(),
160        }
161    }
162}
163
164impl From<CompressionAlgo> for HeaderValue {
165    #[inline]
166    fn from(algo: CompressionAlgo) -> Self {
167        match algo {
168            #[cfg(feature = "brotli")]
169            CompressionAlgo::Brotli => Self::from_static("br"),
170            #[cfg(feature = "deflate")]
171            CompressionAlgo::Deflate => Self::from_static("deflate"),
172            #[cfg(feature = "gzip")]
173            CompressionAlgo::Gzip => Self::from_static("gzip"),
174            #[cfg(feature = "zstd")]
175            CompressionAlgo::Zstd => Self::from_static("zstd"),
176        }
177    }
178}
179
180/// Compression
181#[derive(Clone, Debug)]
182#[non_exhaustive]
183pub struct Compression {
184    /// Compression algorithms to use.
185    pub algos: IndexMap<CompressionAlgo, CompressionLevel>,
186    /// Content types to compress.
187    pub content_types: Vec<Mime>,
188    /// Sets minimum compression size, if body is less than this value, no compression.
189    pub min_length: usize,
190    /// Ignore request algorithms order in `Accept-Encoding` header and always server's config.
191    pub force_priority: bool,
192}
193
194impl Default for Compression {
195    fn default() -> Self {
196        #[allow(unused_mut)]
197        let mut algos = IndexMap::new();
198        #[cfg(feature = "zstd")]
199        algos.insert(CompressionAlgo::Zstd, CompressionLevel::Default);
200        #[cfg(feature = "gzip")]
201        algos.insert(CompressionAlgo::Gzip, CompressionLevel::Default);
202        #[cfg(feature = "deflate")]
203        algos.insert(CompressionAlgo::Deflate, CompressionLevel::Default);
204        #[cfg(feature = "brotli")]
205        algos.insert(CompressionAlgo::Brotli, CompressionLevel::Default);
206        Self {
207            algos,
208            content_types: vec![
209                mime::TEXT_STAR,
210                mime::APPLICATION_JAVASCRIPT,
211                mime::APPLICATION_JSON,
212                mime::IMAGE_SVG,
213                "application/wasm".parse().expect("invalid mime type"),
214                "application/xml".parse().expect("invalid mime type"),
215                "application/rss+xml".parse().expect("invalid mime type"),
216            ],
217            min_length: 0,
218            force_priority: false,
219        }
220    }
221}
222
223impl Compression {
224    /// Create a new `Compression`.
225    #[inline]
226    #[must_use]
227    pub fn new() -> Self {
228        Default::default()
229    }
230
231    /// Remove all compression algorithms.
232    #[inline]
233    #[must_use]
234    pub fn disable_all(mut self) -> Self {
235        self.algos.clear();
236        self
237    }
238
239    /// Sets `Compression` with algos.
240    #[cfg(feature = "gzip")]
241    #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
242    #[inline]
243    #[must_use]
244    pub fn enable_gzip(mut self, level: CompressionLevel) -> Self {
245        self.algos.insert(CompressionAlgo::Gzip, level);
246        self
247    }
248    /// Disable gzip compression.
249    #[cfg(feature = "gzip")]
250    #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
251    #[inline]
252    #[must_use]
253    pub fn disable_gzip(mut self) -> Self {
254        self.algos.shift_remove(&CompressionAlgo::Gzip);
255        self
256    }
257    /// Enable zstd compression.
258    #[cfg(feature = "zstd")]
259    #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
260    #[inline]
261    #[must_use]
262    pub fn enable_zstd(mut self, level: CompressionLevel) -> Self {
263        self.algos.insert(CompressionAlgo::Zstd, level);
264        self
265    }
266    /// Disable zstd compression.
267    #[cfg(feature = "zstd")]
268    #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
269    #[inline]
270    #[must_use]
271    pub fn disable_zstd(mut self) -> Self {
272        self.algos.shift_remove(&CompressionAlgo::Zstd);
273        self
274    }
275    /// Enable brotli compression.
276    #[cfg(feature = "brotli")]
277    #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
278    #[inline]
279    #[must_use]
280    pub fn enable_brotli(mut self, level: CompressionLevel) -> Self {
281        self.algos.insert(CompressionAlgo::Brotli, level);
282        self
283    }
284    /// Disable brotli compression.
285    #[cfg(feature = "brotli")]
286    #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
287    #[inline]
288    #[must_use]
289    pub fn disable_brotli(mut self) -> Self {
290        self.algos.shift_remove(&CompressionAlgo::Brotli);
291        self
292    }
293
294    /// Enable deflate compression.
295    #[cfg(feature = "deflate")]
296    #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
297    #[inline]
298    #[must_use]
299    pub fn enable_deflate(mut self, level: CompressionLevel) -> Self {
300        self.algos.insert(CompressionAlgo::Deflate, level);
301        self
302    }
303
304    /// Disable deflate compression.
305    #[cfg(feature = "deflate")]
306    #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
307    #[inline]
308    #[must_use]
309    pub fn disable_deflate(mut self) -> Self {
310        self.algos.shift_remove(&CompressionAlgo::Deflate);
311        self
312    }
313
314    /// Sets minimum compression size, if body is less than this value, no compression
315    /// default is 1kb
316    #[inline]
317    #[must_use]
318    pub fn min_length(mut self, size: usize) -> Self {
319        self.min_length = size;
320        self
321    }
322    /// Sets `Compression` with force_priority.
323    #[inline]
324    #[must_use]
325    pub fn force_priority(mut self, force_priority: bool) -> Self {
326        self.force_priority = force_priority;
327        self
328    }
329
330    /// Sets `Compression` with content types list.
331    #[inline]
332    #[must_use]
333    pub fn content_types(mut self, content_types: &[Mime]) -> Self {
334        self.content_types = content_types.to_vec();
335        self
336    }
337
338    fn negotiate(
339        &self,
340        req: &Request,
341        res: &Response,
342    ) -> Option<(CompressionAlgo, CompressionLevel)> {
343        if req.headers().contains_key(&CONTENT_ENCODING) {
344            return None;
345        }
346
347        if !self.content_types.is_empty() {
348            let content_type = res
349                .headers()
350                .get(CONTENT_TYPE)
351                .and_then(|v| v.to_str().ok())
352                .unwrap_or_default();
353            if content_type.is_empty() {
354                return None;
355            }
356            if let Ok(content_type) = content_type.parse::<Mime>() {
357                if !self.content_types.iter().any(|citem| {
358                    citem.type_() == content_type.type_()
359                        && (citem.subtype() == "*" || citem.subtype() == content_type.subtype())
360                }) {
361                    return None;
362                }
363            } else {
364                return None;
365            }
366        }
367        let header = req
368            .headers()
369            .get(ACCEPT_ENCODING)
370            .and_then(|v| v.to_str().ok())?;
371
372        let accept_algos = http::parse_accept_encoding(header)
373            .into_iter()
374            .filter_map(|(algo, level)| {
375                if let Ok(algo) = algo.parse::<CompressionAlgo>() {
376                    Some((algo, level))
377                } else {
378                    None
379                }
380            })
381            .collect::<Vec<_>>();
382        if self.force_priority {
383            let accept_algos = accept_algos
384                .into_iter()
385                .map(|(algo, _)| algo)
386                .collect::<Vec<_>>();
387            self.algos
388                .iter()
389                .find(|(algo, _level)| accept_algos.contains(algo))
390                .map(|(algo, level)| (*algo, *level))
391        } else {
392            accept_algos
393                .into_iter()
394                .find_map(|(algo, _)| self.algos.get(&algo).map(|level| (algo, *level)))
395        }
396    }
397}
398
399#[async_trait]
400impl Handler for Compression {
401    async fn handle(
402        &self,
403        req: &mut Request,
404        depot: &mut Depot,
405        res: &mut Response,
406        ctrl: &mut FlowCtrl,
407    ) {
408        ctrl.call_next(req, depot, res).await;
409        if ctrl.is_ceased() || res.headers().contains_key(CONTENT_ENCODING) {
410            return;
411        }
412
413        if let Some(StatusCode::SWITCHING_PROTOCOLS | StatusCode::NO_CONTENT) = res.status_code {
414            return;
415        }
416
417        match res.take_body() {
418            ResBody::None => {
419                return;
420            }
421            ResBody::Once(bytes) => {
422                if self.min_length > 0 && bytes.len() < self.min_length {
423                    res.body(ResBody::Once(bytes));
424                    return;
425                }
426                if let Some((algo, level)) = self.negotiate(req, res) {
427                    res.stream(EncodeStream::new(algo, level, Some(bytes)));
428                    res.headers_mut().append(CONTENT_ENCODING, algo.into());
429                } else {
430                    res.body(ResBody::Once(bytes));
431                    return;
432                }
433            }
434            ResBody::Chunks(chunks) => {
435                if self.min_length > 0 {
436                    let len: usize = chunks.iter().map(|c| c.len()).sum();
437                    if len < self.min_length {
438                        res.body(ResBody::Chunks(chunks));
439                        return;
440                    }
441                }
442                if let Some((algo, level)) = self.negotiate(req, res) {
443                    res.stream(EncodeStream::new(algo, level, chunks));
444                    res.headers_mut().append(CONTENT_ENCODING, algo.into());
445                } else {
446                    res.body(ResBody::Chunks(chunks));
447                    return;
448                }
449            }
450            ResBody::Hyper(body) => {
451                if let Some((algo, level)) = self.negotiate(req, res) {
452                    res.stream(EncodeStream::new(algo, level, body));
453                    res.headers_mut().append(CONTENT_ENCODING, algo.into());
454                } else {
455                    res.body(ResBody::Hyper(body));
456                    return;
457                }
458            }
459            ResBody::Stream(body) => {
460                let body = body.into_inner();
461                if let Some((algo, level)) = self.negotiate(req, res) {
462                    res.stream(EncodeStream::new(algo, level, body));
463                    res.headers_mut().append(CONTENT_ENCODING, algo.into());
464                } else {
465                    res.body(ResBody::stream(body));
466                    return;
467                }
468            }
469            body => {
470                res.body(body);
471                return;
472            }
473        }
474        res.headers_mut().remove(CONTENT_LENGTH);
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use salvo_core::prelude::*;
481    use salvo_core::test::{ResponseExt, TestClient};
482
483    use super::*;
484
485    #[handler]
486    async fn hello() -> &'static str {
487        "hello"
488    }
489
490    #[tokio::test]
491    async fn test_gzip() {
492        let comp_handler = Compression::new().min_length(1);
493        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
494
495        let mut res = TestClient::get("http://127.0.0.1:5801/hello")
496            .add_header(ACCEPT_ENCODING, "gzip", true)
497            .send(router)
498            .await;
499        assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
500        let content = res.take_string().await.unwrap();
501        assert_eq!(content, "hello");
502    }
503
504    #[tokio::test]
505    async fn test_brotli() {
506        let comp_handler = Compression::new().min_length(1);
507        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
508
509        let mut res = TestClient::get("http://127.0.0.1:5801/hello")
510            .add_header(ACCEPT_ENCODING, "br", true)
511            .send(router)
512            .await;
513        assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
514        let content = res.take_string().await.unwrap();
515        assert_eq!(content, "hello");
516    }
517
518    #[tokio::test]
519    async fn test_deflate() {
520        let comp_handler = Compression::new().min_length(1);
521        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
522
523        let mut res = TestClient::get("http://127.0.0.1:5801/hello")
524            .add_header(ACCEPT_ENCODING, "deflate", true)
525            .send(router)
526            .await;
527        assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "deflate");
528        let content = res.take_string().await.unwrap();
529        assert_eq!(content, "hello");
530    }
531
532    #[tokio::test]
533    async fn test_zstd() {
534        let comp_handler = Compression::new().min_length(1);
535        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
536
537        let mut res = TestClient::get("http://127.0.0.1:5801/hello")
538            .add_header(ACCEPT_ENCODING, "zstd", true)
539            .send(router)
540            .await;
541        assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "zstd");
542        let content = res.take_string().await.unwrap();
543        assert_eq!(content, "hello");
544    }
545
546    #[tokio::test]
547    async fn test_min_length_not_compress() {
548        let comp_handler = Compression::new().min_length(10);
549        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
550
551        let res = TestClient::get("http://127.0.0.1:5801/hello")
552            .add_header(ACCEPT_ENCODING, "gzip", true)
553            .send(router)
554            .await;
555        assert!(res.headers().get(CONTENT_ENCODING).is_none());
556    }
557
558    #[tokio::test]
559    async fn test_min_length_should_compress() {
560        let comp_handler = Compression::new().min_length(1);
561        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
562
563        let res = TestClient::get("http://127.0.0.1:5801/hello")
564            .add_header(ACCEPT_ENCODING, "gzip", true)
565            .send(router)
566            .await;
567        assert!(res.headers().get(CONTENT_ENCODING).is_some());
568    }
569
570    #[handler]
571    async fn hello_html(res: &mut Response) {
572        res.render(Text::Html("<html><body>hello</body></html>"));
573    }
574    #[tokio::test]
575    async fn test_content_types_should_compress() {
576        let comp_handler = Compression::new()
577            .min_length(1)
578            .content_types(&[mime::TEXT_HTML]);
579        let router =
580            Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello_html));
581
582        let res = TestClient::get("http://127.0.0.1:5801/hello")
583            .add_header(ACCEPT_ENCODING, "gzip", true)
584            .send(router)
585            .await;
586        assert!(res.headers().get(CONTENT_ENCODING).is_some());
587    }
588
589    #[tokio::test]
590    async fn test_content_types_not_compress() {
591        let comp_handler = Compression::new()
592            .min_length(1)
593            .content_types(&[mime::APPLICATION_JSON]);
594        let router =
595            Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello_html));
596
597        let res = TestClient::get("http://127.0.0.1:5801/hello")
598            .add_header(ACCEPT_ENCODING, "gzip", true)
599            .send(router)
600            .await;
601        assert!(res.headers().get(CONTENT_ENCODING).is_none());
602    }
603
604    #[tokio::test]
605    async fn test_force_priority() {
606        let comp_handler = Compression::new()
607            .disable_all()
608            .enable_brotli(CompressionLevel::Default)
609            .enable_gzip(CompressionLevel::Default)
610            .min_length(1)
611            .force_priority(true);
612        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
613
614        let mut res = TestClient::get("http://127.0.0.1:5801/hello")
615            .add_header(ACCEPT_ENCODING, "gzip, br", true)
616            .send(router)
617            .await;
618        assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
619        let content = res.take_string().await.unwrap();
620        assert_eq!(content, "hello");
621    }
622
623    // Tests for CompressionLevel
624    #[test]
625    fn test_compression_level_default() {
626        let level: CompressionLevel = Default::default();
627        assert_eq!(level, CompressionLevel::Default);
628    }
629
630    #[test]
631    fn test_compression_level_fastest() {
632        let level = CompressionLevel::Fastest;
633        assert_eq!(level, CompressionLevel::Fastest);
634    }
635
636    #[test]
637    fn test_compression_level_minsize() {
638        let level = CompressionLevel::Minsize;
639        assert_eq!(level, CompressionLevel::Minsize);
640    }
641
642    #[test]
643    fn test_compression_level_precise() {
644        let level = CompressionLevel::Precise(5);
645        assert_eq!(level, CompressionLevel::Precise(5));
646    }
647
648    #[test]
649    fn test_compression_level_clone() {
650        let level = CompressionLevel::Fastest;
651        let cloned = level;
652        assert_eq!(level, cloned);
653    }
654
655    #[test]
656    fn test_compression_level_copy() {
657        let level = CompressionLevel::Default;
658        let copied = level;
659        assert_eq!(level, copied);
660    }
661
662    #[test]
663    fn test_compression_level_debug() {
664        let level = CompressionLevel::Fastest;
665        let debug_str = format!("{:?}", level);
666        assert!(debug_str.contains("Fastest"));
667    }
668
669    // Tests for CompressionAlgo
670    #[cfg(feature = "gzip")]
671    #[test]
672    fn test_compression_algo_gzip_from_str() {
673        let algo: CompressionAlgo = "gzip".parse().unwrap();
674        assert_eq!(algo, CompressionAlgo::Gzip);
675    }
676
677    #[cfg(feature = "brotli")]
678    #[test]
679    fn test_compression_algo_brotli_from_str() {
680        let algo: CompressionAlgo = "br".parse().unwrap();
681        assert_eq!(algo, CompressionAlgo::Brotli);
682
683        let algo: CompressionAlgo = "brotli".parse().unwrap();
684        assert_eq!(algo, CompressionAlgo::Brotli);
685    }
686
687    #[cfg(feature = "deflate")]
688    #[test]
689    fn test_compression_algo_deflate_from_str() {
690        let algo: CompressionAlgo = "deflate".parse().unwrap();
691        assert_eq!(algo, CompressionAlgo::Deflate);
692    }
693
694    #[cfg(feature = "zstd")]
695    #[test]
696    fn test_compression_algo_zstd_from_str() {
697        let algo: CompressionAlgo = "zstd".parse().unwrap();
698        assert_eq!(algo, CompressionAlgo::Zstd);
699    }
700
701    #[test]
702    fn test_compression_algo_unknown_from_str() {
703        let result: Result<CompressionAlgo, _> = "unknown".parse();
704        assert!(result.is_err());
705        assert!(
706            result
707                .unwrap_err()
708                .contains("unknown compression algorithm")
709        );
710    }
711
712    #[cfg(feature = "gzip")]
713    #[test]
714    fn test_compression_algo_gzip_display() {
715        let algo = CompressionAlgo::Gzip;
716        assert_eq!(format!("{}", algo), "gzip");
717    }
718
719    #[cfg(feature = "brotli")]
720    #[test]
721    fn test_compression_algo_brotli_display() {
722        let algo = CompressionAlgo::Brotli;
723        assert_eq!(format!("{}", algo), "br");
724    }
725
726    #[cfg(feature = "deflate")]
727    #[test]
728    fn test_compression_algo_deflate_display() {
729        let algo = CompressionAlgo::Deflate;
730        assert_eq!(format!("{}", algo), "deflate");
731    }
732
733    #[cfg(feature = "zstd")]
734    #[test]
735    fn test_compression_algo_zstd_display() {
736        let algo = CompressionAlgo::Zstd;
737        assert_eq!(format!("{}", algo), "zstd");
738    }
739
740    #[cfg(feature = "gzip")]
741    #[test]
742    fn test_compression_algo_into_header_value() {
743        let algo = CompressionAlgo::Gzip;
744        let header: HeaderValue = algo.into();
745        assert_eq!(header, "gzip");
746    }
747
748    #[test]
749    fn test_compression_algo_debug() {
750        #[cfg(feature = "gzip")]
751        {
752            let algo = CompressionAlgo::Gzip;
753            let debug_str = format!("{:?}", algo);
754            assert!(debug_str.contains("Gzip"));
755        }
756    }
757
758    #[test]
759    fn test_compression_algo_clone() {
760        #[cfg(feature = "gzip")]
761        {
762            let algo = CompressionAlgo::Gzip;
763            let cloned = algo;
764            assert_eq!(algo, cloned);
765        }
766    }
767
768    #[test]
769    fn test_compression_algo_hash() {
770        use std::collections::HashSet;
771        #[cfg(feature = "gzip")]
772        {
773            let mut set = HashSet::new();
774            set.insert(CompressionAlgo::Gzip);
775            assert!(set.contains(&CompressionAlgo::Gzip));
776        }
777    }
778
779    // Tests for Compression struct
780    #[test]
781    fn test_compression_new() {
782        let comp = Compression::new();
783        assert!(!comp.algos.is_empty());
784        assert!(!comp.content_types.is_empty());
785        assert_eq!(comp.min_length, 0);
786        assert!(!comp.force_priority);
787    }
788
789    #[test]
790    fn test_compression_default() {
791        let comp = Compression::default();
792        assert!(!comp.algos.is_empty());
793    }
794
795    #[test]
796    fn test_compression_disable_all() {
797        let comp = Compression::new().disable_all();
798        assert!(comp.algos.is_empty());
799    }
800
801    #[cfg(feature = "gzip")]
802    #[test]
803    fn test_compression_enable_gzip() {
804        let comp = Compression::new()
805            .disable_all()
806            .enable_gzip(CompressionLevel::Fastest);
807        assert!(comp.algos.contains_key(&CompressionAlgo::Gzip));
808        assert_eq!(
809            comp.algos.get(&CompressionAlgo::Gzip),
810            Some(&CompressionLevel::Fastest)
811        );
812    }
813
814    #[cfg(feature = "gzip")]
815    #[test]
816    fn test_compression_disable_gzip() {
817        let comp = Compression::new().disable_gzip();
818        assert!(!comp.algos.contains_key(&CompressionAlgo::Gzip));
819    }
820
821    #[cfg(feature = "brotli")]
822    #[test]
823    fn test_compression_enable_brotli() {
824        let comp = Compression::new()
825            .disable_all()
826            .enable_brotli(CompressionLevel::Minsize);
827        assert!(comp.algos.contains_key(&CompressionAlgo::Brotli));
828    }
829
830    #[cfg(feature = "brotli")]
831    #[test]
832    fn test_compression_disable_brotli() {
833        let comp = Compression::new().disable_brotli();
834        assert!(!comp.algos.contains_key(&CompressionAlgo::Brotli));
835    }
836
837    #[cfg(feature = "zstd")]
838    #[test]
839    fn test_compression_enable_zstd() {
840        let comp = Compression::new()
841            .disable_all()
842            .enable_zstd(CompressionLevel::Default);
843        assert!(comp.algos.contains_key(&CompressionAlgo::Zstd));
844    }
845
846    #[cfg(feature = "zstd")]
847    #[test]
848    fn test_compression_disable_zstd() {
849        let comp = Compression::new().disable_zstd();
850        assert!(!comp.algos.contains_key(&CompressionAlgo::Zstd));
851    }
852
853    #[cfg(feature = "deflate")]
854    #[test]
855    fn test_compression_enable_deflate() {
856        let comp = Compression::new()
857            .disable_all()
858            .enable_deflate(CompressionLevel::Default);
859        assert!(comp.algos.contains_key(&CompressionAlgo::Deflate));
860    }
861
862    #[cfg(feature = "deflate")]
863    #[test]
864    fn test_compression_disable_deflate() {
865        let comp = Compression::new().disable_deflate();
866        assert!(!comp.algos.contains_key(&CompressionAlgo::Deflate));
867    }
868
869    #[test]
870    fn test_compression_min_length() {
871        let comp = Compression::new().min_length(1024);
872        assert_eq!(comp.min_length, 1024);
873    }
874
875    #[test]
876    fn test_compression_force_priority() {
877        let comp = Compression::new().force_priority(true);
878        assert!(comp.force_priority);
879    }
880
881    #[test]
882    fn test_compression_content_types() {
883        let comp = Compression::new().content_types(&[mime::TEXT_PLAIN, mime::TEXT_HTML]);
884        assert_eq!(comp.content_types.len(), 2);
885        assert!(comp.content_types.contains(&mime::TEXT_PLAIN));
886        assert!(comp.content_types.contains(&mime::TEXT_HTML));
887    }
888
889    #[test]
890    fn test_compression_debug() {
891        let comp = Compression::new();
892        let debug_str = format!("{:?}", comp);
893        assert!(debug_str.contains("Compression"));
894        assert!(debug_str.contains("algos"));
895        assert!(debug_str.contains("content_types"));
896    }
897
898    #[test]
899    fn test_compression_clone() {
900        let comp = Compression::new().min_length(100);
901        let cloned = comp.clone();
902        assert_eq!(comp.min_length, cloned.min_length);
903        assert_eq!(comp.algos.len(), cloned.algos.len());
904    }
905
906    // Tests for no compression scenarios
907    #[tokio::test]
908    async fn test_no_accept_encoding_header() {
909        let comp_handler = Compression::new().min_length(1);
910        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
911
912        let res = TestClient::get("http://127.0.0.1:5801/hello")
913            .send(router)
914            .await;
915        assert!(res.headers().get(CONTENT_ENCODING).is_none());
916    }
917
918    #[tokio::test]
919    async fn test_unsupported_encoding() {
920        let comp_handler = Compression::new().min_length(1);
921        let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
922
923        let res = TestClient::get("http://127.0.0.1:5801/hello")
924            .add_header(ACCEPT_ENCODING, "unknown", true)
925            .send(router)
926            .await;
927        assert!(res.headers().get(CONTENT_ENCODING).is_none());
928    }
929
930    #[tokio::test]
931    async fn test_empty_response() {
932        #[handler]
933        async fn empty() {}
934
935        let comp_handler = Compression::new();
936        let router = Router::with_hoop(comp_handler).push(Router::with_path("empty").get(empty));
937
938        let res = TestClient::get("http://127.0.0.1:5801/empty")
939            .add_header(ACCEPT_ENCODING, "gzip", true)
940            .send(router)
941            .await;
942        assert!(res.headers().get(CONTENT_ENCODING).is_none());
943    }
944
945    #[tokio::test]
946    async fn test_chained_configuration() {
947        #[cfg(all(feature = "gzip", feature = "brotli"))]
948        {
949            let comp_handler = Compression::new()
950                .disable_all()
951                .enable_gzip(CompressionLevel::Fastest)
952                .enable_brotli(CompressionLevel::Default)
953                .min_length(1)
954                .force_priority(false)
955                .content_types(&[mime::TEXT_PLAIN]);
956
957            assert_eq!(comp_handler.algos.len(), 2);
958            assert_eq!(comp_handler.min_length, 1);
959            assert!(!comp_handler.force_priority);
960            assert_eq!(comp_handler.content_types.len(), 1);
961        }
962    }
963}