1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use 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#[non_exhaustive]
82#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
83pub enum CompressionLevel {
84 Fastest,
86 Minsize,
88 #[default]
90 Default,
91 Precise(u32),
96}
97
98#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash)]
100#[non_exhaustive]
101pub enum CompressionAlgo {
102 #[cfg(feature = "brotli")]
104 #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
105 Brotli,
106
107 #[cfg(feature = "deflate")]
109 #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
110 Deflate,
111
112 #[cfg(feature = "gzip")]
114 #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
115 Gzip,
116
117 #[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#[derive(Clone, Debug)]
182#[non_exhaustive]
183pub struct Compression {
184 pub algos: IndexMap<CompressionAlgo, CompressionLevel>,
186 pub content_types: Vec<Mime>,
188 pub min_length: usize,
190 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 #[inline]
226 #[must_use]
227 pub fn new() -> Self {
228 Default::default()
229 }
230
231 #[inline]
233 #[must_use]
234 pub fn disable_all(mut self) -> Self {
235 self.algos.clear();
236 self
237 }
238
239 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
317 #[must_use]
318 pub fn min_length(mut self, size: usize) -> Self {
319 self.min_length = size;
320 self
321 }
322 #[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 #[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 #[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 #[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 #[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 #[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}