1#![cfg(feature = "server")]
7
8use std::io;
9use std::sync::LazyLock;
10
11use axum::body::{Body, BodyDataStream, HttpBody};
12use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
13use axum::response::Response;
14use bytes::Bytes;
15use compression_codecs::core::util::{PartialBuffer, WriteBuffer};
16use compression_codecs::core::Level;
17use compression_codecs::{
18 brotli::params::EncoderParams as BrotliParams, flate::params::FlateEncoderParams,
19 zstd::params::CParameter, BrotliEncoder, EncodeV2, GzipEncoder, ZlibEncoder, ZstdEncoder,
20};
21use futures::StreamExt;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(crate) struct CompressionAlgorithms {
26 pub(crate) gzip: bool,
27 pub(crate) brotli: bool,
28 pub(crate) deflate: bool,
29 pub(crate) zstd: bool,
30}
31
32impl CompressionAlgorithms {
33 pub(crate) fn all_enabled() -> Self {
34 Self {
35 gzip: true,
36 brotli: true,
37 deflate: true,
38 zstd: true,
39 }
40 }
41
42 fn is_empty(&self) -> bool {
43 !self.gzip && !self.brotli && !self.deflate && !self.zstd
44 }
45
46 fn supports(&self, encoding: Encoding) -> bool {
47 match encoding {
48 Encoding::Identity => true,
49 Encoding::Deflate => self.deflate,
50 Encoding::Gzip => self.gzip,
51 Encoding::Brotli => self.brotli,
52 Encoding::Zstd => self.zstd,
53 }
54 }
55
56 pub(crate) fn encodings(self) -> impl Iterator<Item = Encoding> {
58 Encoding::COMPRESSED
59 .into_iter()
60 .filter(move |encoding| self.supports(*encoding))
61 }
62}
63
64pub(crate) fn parse_compression_algorithms(env: &str) -> Option<CompressionAlgorithms> {
67 let env = env.trim();
68 if env.is_empty() || env.eq_ignore_ascii_case("none") || env.eq_ignore_ascii_case("off") {
69 return None;
70 }
71
72 let mut all = false;
73 let mut gzip = false;
74 let mut brotli = false;
75 let mut deflate = false;
76 let mut zstd = false;
77
78 for part in env.split(',') {
79 match part.trim().to_lowercase().as_str() {
80 "all" => all = true,
81 "gzip" => gzip = true,
82 "brotli" | "br" => brotli = true,
83 "deflate" => deflate = true,
84 "zstd" => zstd = true,
85 other => tracing::warn!(
86 "Unknown compression algorithm in COMPRESSION_ALGORITHMS: '{}'",
87 other
88 ),
89 }
90 }
91
92 if all {
93 return Some(CompressionAlgorithms::all_enabled());
94 }
95
96 let algorithms = CompressionAlgorithms {
97 gzip,
98 brotli,
99 deflate,
100 zstd,
101 };
102 if algorithms.is_empty() {
103 return None;
104 }
105
106 Some(algorithms)
107}
108
109static ALGORITHMS: LazyLock<Option<CompressionAlgorithms>> = LazyLock::new(|| {
110 let env = std::env::var("COMPRESSION_ALGORITHMS").unwrap_or_default();
111 parse_compression_algorithms(&env)
112});
113
114pub(crate) fn algorithms() -> Option<CompressionAlgorithms> {
116 *ALGORITHMS
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
121pub(crate) enum Encoding {
122 Identity,
123 Deflate,
124 Gzip,
125 Brotli,
126 Zstd,
127}
128
129impl Encoding {
130 const COMPRESSED: [Encoding; 4] = [
131 Encoding::Deflate,
132 Encoding::Gzip,
133 Encoding::Brotli,
134 Encoding::Zstd,
135 ];
136
137 fn parse(token: &str) -> Option<Self> {
138 [
139 ("identity", Encoding::Identity),
140 ("deflate", Encoding::Deflate),
141 ("gzip", Encoding::Gzip),
142 ("x-gzip", Encoding::Gzip),
143 ("br", Encoding::Brotli),
144 ("zstd", Encoding::Zstd),
145 ]
146 .into_iter()
147 .find_map(|(name, encoding)| token.eq_ignore_ascii_case(name).then_some(encoding))
148 }
149
150 pub(crate) fn header_value(self) -> HeaderValue {
151 HeaderValue::from_static(match self {
152 Encoding::Identity => "identity",
153 Encoding::Deflate => "deflate",
154 Encoding::Gzip => "gzip",
155 Encoding::Brotli => "br",
156 Encoding::Zstd => "zstd",
157 })
158 }
159}
160
161fn parse_qvalue(param: &str) -> Option<u16> {
163 let value = param
164 .strip_prefix("q=")
165 .or_else(|| param.strip_prefix("Q="))?;
166 let (int, frac) = value.split_once('.').unwrap_or((value, ""));
167 let base = match int {
168 "0" => 0,
169 "1" => 1000,
170 _ => return None,
171 };
172 if frac.len() > 3 || !frac.bytes().all(|b| b.is_ascii_digit()) {
173 return None;
174 }
175 let frac = frac
176 .bytes()
177 .zip([100, 10, 1])
178 .map(|(digit, scale)| u16::from(digit - b'0') * scale)
179 .sum::<u16>();
180 let q = base + frac;
181 (q <= 1000).then_some(q)
182}
183
184pub(crate) fn negotiate(
189 headers: &HeaderMap,
190 algorithms: CompressionAlgorithms,
191) -> Option<Encoding> {
192 let mut explicit: Vec<(Encoding, u16)> = Vec::new();
193 let mut wildcard = None;
194 let entries = headers
195 .get_all(header::ACCEPT_ENCODING)
196 .iter()
197 .filter_map(|value| value.to_str().ok())
198 .flat_map(|value| value.split(','));
199 for entry in entries {
200 let (token, param) = match entry.split_once(';') {
201 Some((token, param)) => (token.trim(), Some(param.trim())),
202 None => (entry.trim(), None),
203 };
204 let Some(q) = param.map_or(Some(1000), parse_qvalue) else {
205 continue;
206 };
207 if token == "*" {
208 wildcard.get_or_insert(q);
209 } else if let Some(encoding) = Encoding::parse(token) {
210 if algorithms.supports(encoding) {
211 explicit.push((encoding, q));
212 }
213 }
214 }
215
216 let best = |candidates: &mut dyn Iterator<Item = (Encoding, u16)>| {
217 candidates
218 .filter(|(_, q)| *q > 0)
219 .max_by_key(|&(encoding, q)| (q, encoding))
220 .map(|(encoding, _)| encoding)
221 };
222 match wildcard {
223 None => best(&mut explicit.iter().copied()).or_else(|| {
224 let identity_rejected = explicit
225 .iter()
226 .any(|&(encoding, q)| encoding == Encoding::Identity && q == 0);
227 (!identity_rejected).then_some(Encoding::Identity)
228 }),
229 Some(wildcard) => best(
230 &mut std::iter::once(Encoding::Identity)
231 .chain(algorithms.encodings())
232 .map(|encoding| {
233 let q = explicit
234 .iter()
235 .find(|(listed, _)| *listed == encoding)
236 .map_or(wildcard, |&(_, q)| q);
237 (encoding, q)
238 }),
239 ),
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub(crate) enum Effort {
246 Dynamic,
249 Cached,
251}
252
253impl Effort {
254 fn level(self, encoding: Encoding) -> Level {
255 Level::Precise(match (self, encoding) {
256 (Effort::Dynamic, _) => 7,
257 (Effort::Cached, Encoding::Brotli) => 11,
258 (Effort::Cached, Encoding::Zstd) => 19,
259 (Effort::Cached, _) => 9,
260 })
261 }
262}
263
264fn brotli_window(len: Option<usize>) -> i32 {
268 const MIN: i32 = 18;
269 const MAX: i32 = 22;
270 len.map_or(MAX, |len| {
272 let bits = (len + 16).next_power_of_two().trailing_zeros();
273 (bits as i32).clamp(MIN, MAX)
274 })
275}
276
277pub(crate) struct Encoder {
279 codec: Box<dyn EncodeV2 + Send>,
280 scratch: Box<[u8]>,
281}
282
283impl Encoder {
284 pub(crate) fn new(encoding: Encoding, effort: Effort, len: Option<usize>) -> Option<Self> {
286 let level = effort.level(encoding);
287 let codec: Box<dyn EncodeV2 + Send> = match encoding {
288 Encoding::Identity => return None,
289 Encoding::Deflate => Box::new(ZlibEncoder::new(FlateEncoderParams::from(level))),
290 Encoding::Gzip => Box::new(GzipEncoder::new(FlateEncoderParams::from(level))),
291 Encoding::Brotli => Box::new(BrotliEncoder::new(
292 BrotliParams::default()
293 .quality(level)
294 .window_size(brotli_window(len)),
295 )),
296 Encoding::Zstd => Box::new(ZstdEncoder::new(CParameter::quality(level))),
297 };
298 Some(Self {
299 codec,
300 scratch: vec![0; 32 * 1024].into_boxed_slice(),
301 })
302 }
303
304 pub(crate) fn write(&mut self, input: &[u8], out: &mut Vec<u8>) -> io::Result<()> {
305 let mut input = PartialBuffer::new(input);
306 while !input.unwritten().is_empty() {
307 let mut output = WriteBuffer::new_initialized(&mut self.scratch);
308 self.codec.encode(&mut input, &mut output)?;
309 out.extend_from_slice(output.written());
310 }
311 Ok(())
312 }
313
314 pub(crate) fn flush(&mut self, out: &mut Vec<u8>) -> io::Result<()> {
316 loop {
317 let mut output = WriteBuffer::new_initialized(&mut self.scratch);
318 let done = self.codec.flush(&mut output)?;
319 out.extend_from_slice(output.written());
320 if done {
321 return Ok(());
322 }
323 }
324 }
325
326 pub(crate) fn finish(mut self, out: &mut Vec<u8>) -> io::Result<()> {
327 loop {
328 let mut output = WriteBuffer::new_initialized(&mut self.scratch);
329 let done = self.codec.finish(&mut output)?;
330 out.extend_from_slice(output.written());
331 if done {
332 return Ok(());
333 }
334 }
335 }
336}
337
338pub(crate) fn encode(
340 encoding: Encoding,
341 effort: Effort,
342 data: &[u8],
343) -> Option<io::Result<Vec<u8>>> {
344 let mut encoder = Encoder::new(encoding, effort, Some(data.len()))?;
345 let mut out = Vec::new();
346 Some(
347 encoder
348 .write(data, &mut out)
349 .and_then(|()| encoder.finish(&mut out))
350 .map(|()| out),
351 )
352}
353
354const BUFFER_LIMIT: usize = 1024 * 1024;
356
357pub(crate) async fn compress(
367 algorithms: CompressionAlgorithms,
368 req: axum::extract::Request,
369 next: axum::middleware::Next,
370) -> Response {
371 let encoding = negotiate(req.headers(), algorithms);
372 let mut response = next.run(req).await;
373 let Some(encoding) = encoding else {
374 *response.status_mut() = StatusCode::NOT_ACCEPTABLE;
375 add_vary(response.headers_mut());
376 return response;
377 };
378 if !should_compress(&response) {
379 return response;
380 }
381 add_vary(response.headers_mut());
382 if encoding == Encoding::Identity {
383 return response;
384 }
385 let known_len = response
386 .body()
387 .size_hint()
388 .exact()
389 .and_then(|len| usize::try_from(len).ok());
390 let (mut parts, body) = response.into_parts();
391 parts.headers.remove(header::ACCEPT_RANGES);
392 parts.headers.remove(header::CONTENT_LENGTH);
393 parts
394 .headers
395 .insert(header::CONTENT_ENCODING, encoding.header_value());
396 let stream = CompressStream {
397 input: body.into_data_stream(),
398 encoding,
399 stage: match known_len {
400 Some(len) => Stage::Streaming {
401 encoder: dynamic_encoder(encoding, Some(len)),
402 pending: false,
403 },
404 None => Stage::Buffering(Vec::new()),
405 },
406 };
407 let stream = futures::stream::unfold(stream, |mut stream| async move {
408 stream.next_chunk().await.map(|chunk| (chunk, stream))
409 })
410 .fuse();
412 Response::from_parts(parts, Body::from_stream(stream))
413}
414
415fn add_vary(headers: &mut HeaderMap) {
416 let listed = headers
417 .get_all(header::VARY)
418 .iter()
419 .filter_map(|value| value.to_str().ok())
420 .flat_map(|value| value.split(','))
421 .any(|name| name.trim().eq_ignore_ascii_case("accept-encoding"));
422 if !listed {
423 headers.append(header::VARY, HeaderValue::from_static("accept-encoding"));
424 }
425}
426
427fn should_compress(response: &Response) -> bool {
428 let headers = response.headers();
429 if headers.contains_key(header::CONTENT_ENCODING) || headers.contains_key(header::CONTENT_RANGE)
430 {
431 return false;
432 }
433 let size = response.body().size_hint().exact().or_else(|| {
434 headers
435 .get(header::CONTENT_LENGTH)
436 .and_then(|value| value.to_str().ok())
437 .and_then(|value| value.parse().ok())
438 });
439 if size.is_some_and(|size| size < 32) {
440 return false;
441 }
442 let content_type = headers
443 .get(header::CONTENT_TYPE)
444 .and_then(|value| value.to_str().ok())
445 .unwrap_or_default();
446 let excluded = |prefix: &str, exception: Option<&str>| {
447 content_type.starts_with(prefix)
448 && !exception.is_some_and(|exception| content_type.starts_with(exception))
449 };
450 !(excluded("application/grpc", Some("application/grpc-web"))
451 || excluded("image/", Some("image/svg+xml"))
452 || excluded("text/event-stream", None))
453}
454
455fn dynamic_encoder(encoding: Encoding, len: Option<usize>) -> Encoder {
456 Encoder::new(encoding, Effort::Dynamic, len).expect("identity 不进入压缩流")
457}
458
459enum Stage {
460 Buffering(Vec<u8>),
462 Streaming {
464 encoder: Encoder,
465 pending: bool,
466 },
467 Done,
468}
469
470struct CompressStream {
471 input: BodyDataStream,
472 encoding: Encoding,
473 stage: Stage,
474}
475
476impl CompressStream {
477 async fn next_chunk(&mut self) -> Option<Result<Bytes, axum::Error>> {
479 use futures::FutureExt;
480
481 while !matches!(self.stage, Stage::Done) {
482 let mut out = Vec::new();
483 let next = match self.input.next().now_or_never() {
484 Some(next) => next,
485 None => {
486 if let Err(error) = self.flush(&mut out) {
488 self.stage = Stage::Done;
489 return Some(Err(axum::Error::new(error)));
490 }
491 if !out.is_empty() {
492 return Some(Ok(Bytes::from(out)));
493 }
494 self.input.next().await
495 }
496 };
497 let result = match next {
498 Some(Ok(chunk)) => self.push(&chunk, &mut out),
499 Some(Err(error)) => {
500 self.stage = Stage::Done;
501 return Some(Err(error));
502 }
503 None => self.finish(&mut out),
504 };
505 if let Err(error) = result {
506 self.stage = Stage::Done;
507 return Some(Err(axum::Error::new(error)));
508 }
509 if !out.is_empty() {
510 return Some(Ok(Bytes::from(out)));
511 }
512 }
513 None
514 }
515
516 fn start_streaming(&mut self, buffered: &[u8], out: &mut Vec<u8>) -> io::Result<()> {
518 let mut encoder = dynamic_encoder(self.encoding, None);
519 let result = encoder.write(buffered, out);
520 self.stage = Stage::Streaming {
521 encoder,
522 pending: !buffered.is_empty(),
523 };
524 result
525 }
526
527 fn push(&mut self, chunk: &[u8], out: &mut Vec<u8>) -> io::Result<()> {
528 match &mut self.stage {
529 Stage::Buffering(buffered) => {
530 buffered.extend_from_slice(chunk);
531 if buffered.len() <= BUFFER_LIMIT {
532 return Ok(());
533 }
534 let buffered = std::mem::take(buffered);
535 self.start_streaming(&buffered, out)
536 }
537 Stage::Streaming { encoder, pending } => {
538 *pending |= !chunk.is_empty();
539 encoder.write(chunk, out)
540 }
541 Stage::Done => Ok(()),
542 }
543 }
544
545 fn flush(&mut self, out: &mut Vec<u8>) -> io::Result<()> {
546 match &mut self.stage {
547 Stage::Buffering(buffered) if !buffered.is_empty() => {
548 let buffered = std::mem::take(buffered);
549 self.start_streaming(&buffered, out)?;
550 self.flush(out)
551 }
552 Stage::Streaming { encoder, pending } if *pending => {
553 *pending = false;
554 encoder.flush(out)
555 }
556 _ => Ok(()),
557 }
558 }
559
560 fn finish(&mut self, out: &mut Vec<u8>) -> io::Result<()> {
561 match std::mem::replace(&mut self.stage, Stage::Done) {
562 Stage::Buffering(buffered) => {
563 let mut encoder = dynamic_encoder(self.encoding, Some(buffered.len()));
564 encoder.write(&buffered, out)?;
565 encoder.finish(out)
566 }
567 Stage::Streaming { encoder, .. } => encoder.finish(out),
568 Stage::Done => Ok(()),
569 }
570 }
571}
572
573#[cfg(test)]
574pub(crate) fn decode(encoding: Encoding, data: &[u8]) -> Vec<u8> {
575 use std::io::Read;
576 let mut out = Vec::new();
577 match encoding {
578 Encoding::Identity => out.extend_from_slice(data),
579 Encoding::Deflate => {
580 flate2::read::ZlibDecoder::new(data)
581 .read_to_end(&mut out)
582 .unwrap();
583 }
584 Encoding::Gzip => {
585 flate2::read::GzDecoder::new(data)
586 .read_to_end(&mut out)
587 .unwrap();
588 }
589 Encoding::Brotli => {
590 let mut decoder = compression_codecs::BrotliDecoder::new();
591 decode_with(&mut decoder, data, &mut out);
592 }
593 Encoding::Zstd => {
594 let mut decoder = compression_codecs::ZstdDecoder::new();
595 decode_with(&mut decoder, data, &mut out);
596 }
597 }
598 out
599}
600
601#[cfg(test)]
602fn decode_with(decoder: &mut dyn compression_codecs::DecodeV2, data: &[u8], out: &mut Vec<u8>) {
603 let mut input = PartialBuffer::new(data);
604 let mut scratch = vec![0; 32 * 1024];
605 loop {
606 let mut output = WriteBuffer::new_initialized(&mut scratch);
607 let done = decoder.decode(&mut input, &mut output).unwrap();
608 out.extend_from_slice(output.written());
609 if done {
610 return;
611 }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn compression_all_enables_everything() {
621 assert_eq!(
622 parse_compression_algorithms("all"),
623 Some(CompressionAlgorithms::all_enabled())
624 );
625 }
626
627 #[test]
628 fn compression_empty_none_off_disable() {
629 for value in ["", "none", "NONE", "off", "OFF"] {
630 assert_eq!(parse_compression_algorithms(value), None, "{value}");
631 }
632 }
633
634 #[test]
635 fn compression_single_and_multiple_algorithms() {
636 assert_eq!(
637 parse_compression_algorithms("br"),
638 Some(CompressionAlgorithms {
639 gzip: false,
640 brotli: true,
641 deflate: false,
642 zstd: false,
643 })
644 );
645 assert_eq!(
646 parse_compression_algorithms("gzip, zstd"),
647 Some(CompressionAlgorithms {
648 gzip: true,
649 brotli: false,
650 deflate: false,
651 zstd: true,
652 })
653 );
654 }
655
656 #[test]
657 fn compression_case_insensitive_and_whitespace_tolerant() {
658 assert_eq!(
659 parse_compression_algorithms(" GZIP , Brotli , deflate , Zstd "),
660 Some(CompressionAlgorithms::all_enabled())
661 );
662 }
663
664 #[test]
665 fn compression_unknown_algorithms_are_ignored() {
666 assert_eq!(
667 parse_compression_algorithms("gzip, unknown, lz4"),
668 Some(CompressionAlgorithms {
669 gzip: true,
670 brotli: false,
671 deflate: false,
672 zstd: false,
673 })
674 );
675 }
676
677 fn pick(config: &str, accept: &str) -> Option<Encoding> {
678 let mut headers = HeaderMap::new();
679 headers.insert(
680 header::ACCEPT_ENCODING,
681 HeaderValue::from_str(accept).unwrap(),
682 );
683 negotiate(&headers, parse_compression_algorithms(config).unwrap())
684 }
685
686 #[test]
687 fn negotiation_prefers_recommended_brotli_and_falls_back() {
688 let chrome = "gzip, deflate, br, zstd";
689 assert_eq!(pick("br, gzip", chrome), Some(Encoding::Brotli));
690 assert_eq!(pick("all", chrome), Some(Encoding::Zstd));
691 assert_eq!(pick("br, gzip", "gzip"), Some(Encoding::Gzip));
692 assert_eq!(pick("br, gzip", "zstd"), Some(Encoding::Identity));
693 assert_eq!(pick("br, gzip", ""), Some(Encoding::Identity));
694 }
695
696 #[test]
697 fn negotiation_honours_qvalues() {
698 assert_eq!(
699 pick("br, gzip", "br;q=0.5, gzip;q=0.8"),
700 Some(Encoding::Gzip)
701 );
702 assert_eq!(pick("br, gzip", "br;q=0, gzip"), Some(Encoding::Gzip));
703 assert_eq!(
704 pick("br, gzip", "br;q=1.0, gzip;q=1"),
705 Some(Encoding::Brotli)
706 );
707 assert_eq!(
709 pick("br, gzip", "br;q=2, gzip;q=0.1234"),
710 Some(Encoding::Identity)
711 );
712 assert_eq!(pick("br, gzip", "br;q=0"), Some(Encoding::Identity));
714 assert_eq!(pick("br, gzip", "identity;q=0"), None);
715 }
716
717 #[test]
718 fn negotiation_applies_wildcard_to_unlisted_encodings() {
719 assert_eq!(pick("br, gzip", "*"), Some(Encoding::Brotli));
720 assert_eq!(pick("br, gzip", "gzip, *;q=0"), Some(Encoding::Gzip));
721 assert_eq!(pick("br, gzip", "*;q=0"), None);
722 assert_eq!(
723 pick("br, gzip", "identity, *;q=0"),
724 Some(Encoding::Identity)
725 );
726 assert_eq!(pick("br, gzip", "br;q=0.1, *;q=0.5"), Some(Encoding::Gzip));
727 }
728
729 #[test]
730 fn encoders_round_trip_for_every_encoding() {
731 let page =
732 "<article><p>压缩回归测试段落 compression regression.</p></article>".repeat(3000);
733 for encoding in Encoding::COMPRESSED {
734 let encoded = encode(encoding, Effort::Cached, page.as_bytes())
735 .unwrap()
736 .unwrap();
737 assert!(encoded.len() < page.len() / 10, "{encoding:?}");
738 assert_eq!(decode(encoding, &encoded), page.as_bytes(), "{encoding:?}");
739 }
740 assert!(encode(Encoding::Identity, Effort::Cached, b"x").is_none());
741 }
742
743 #[test]
744 fn brotli_window_covers_whole_known_input() {
745 assert_eq!(brotli_window(None), 22);
746 assert_eq!(brotli_window(Some(0)), 18);
747 assert_eq!(brotli_window(Some((1 << 18) - 16)), 18);
748 assert_eq!(brotli_window(Some((1 << 18) - 15)), 19);
749 assert_eq!(brotli_window(Some((1 << 21) - 15)), 22);
750 assert_eq!(brotli_window(Some(1 << 30)), 22);
751 }
752
753 type Respond = fn() -> Response;
755
756 async fn through_middleware(config: &str, accept: &str, respond: Respond) -> Response {
758 use tower::ServiceExt;
759 let algorithms = parse_compression_algorithms(config).unwrap();
760 axum::Router::new()
761 .route("/", axum::routing::get(move || async move { respond() }))
762 .layer(axum::middleware::from_fn(
763 move |req: axum::extract::Request, next: axum::middleware::Next| {
764 compress(algorithms, req, next)
765 },
766 ))
767 .oneshot(
768 axum::http::Request::get("/")
769 .header(header::ACCEPT_ENCODING, accept)
770 .body(Body::empty())
771 .unwrap(),
772 )
773 .await
774 .unwrap()
775 }
776
777 fn page(paragraphs: usize) -> String {
778 (0..paragraphs)
779 .map(|i| {
780 format!(
781 "<p>第 {i} 段 dynamic compression regression {}</p>",
782 i * 7919
783 )
784 })
785 .collect()
786 }
787
788 fn html(body: Body) -> Response {
789 Response::builder()
790 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
791 .body(body)
792 .unwrap()
793 }
794
795 const BODIES: [(&str, Respond); 3] = [
797 ("known", || html(Body::from(page(200)))),
798 ("chunked", || {
799 let chunks = page(200)
800 .into_bytes()
801 .chunks(700)
802 .map(<[u8]>::to_vec)
803 .collect::<Vec<_>>();
804 html(Body::from_stream(futures::stream::iter(
805 chunks.into_iter().map(Ok::<_, io::Error>),
806 )))
807 }),
808 ("over-limit", || {
809 let chunks = page(30_000)
810 .into_bytes()
811 .chunks(64 * 1024)
812 .map(<[u8]>::to_vec)
813 .collect::<Vec<_>>();
814 html(Body::from_stream(futures::stream::iter(
815 chunks.into_iter().map(Ok::<_, io::Error>),
816 )))
817 }),
818 ];
819
820 #[tokio::test]
821 async fn dynamic_compression_round_trips_buffered_and_streamed_bodies() {
822 assert!(page(30_000).len() > BUFFER_LIMIT);
823 for (config, accept, expected) in [
824 ("br, gzip", "gzip, deflate, br, zstd", Encoding::Brotli),
825 ("br, gzip", "gzip", Encoding::Gzip),
826 ("all", "gzip, deflate, br, zstd", Encoding::Zstd),
827 ] {
828 for (name, respond) in BODIES {
829 let expected_body = axum::body::to_bytes(respond().into_body(), usize::MAX)
830 .await
831 .unwrap();
832 let response = through_middleware(config, accept, respond).await;
833 let headers = response.headers();
834 assert_eq!(
835 headers[header::CONTENT_ENCODING],
836 expected.header_value(),
837 "{name} {accept}"
838 );
839 assert_eq!(headers[header::VARY], "accept-encoding", "{name}");
840 assert!(!headers.contains_key(header::CONTENT_LENGTH), "{name}");
841 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
842 .await
843 .unwrap();
844 assert_eq!(decode(expected, &body), expected_body, "{name} {accept}");
845 }
846 }
847 }
848
849 #[tokio::test]
850 async fn dynamic_compression_skips_ineligible_responses() {
851 fn typed(content_type: &'static str) -> Response {
852 Response::builder()
853 .header(header::CONTENT_TYPE, content_type)
854 .body(Body::from(page(50)))
855 .unwrap()
856 }
857 let cases: [(&str, Respond, bool); 6] = [
858 ("png", || typed("image/png"), false),
859 ("svg", || typed("image/svg+xml"), true),
860 ("sse", || typed("text/event-stream"), false),
861 ("tiny", || html(Body::from("<p>短</p>")), false),
862 (
863 "precompressed",
864 || {
865 let mut response = typed("text/css");
866 response
867 .headers_mut()
868 .insert(header::CONTENT_ENCODING, HeaderValue::from_static("br"));
869 response
870 },
871 false,
872 ),
873 (
874 "range",
875 || {
876 let mut response = typed("text/plain");
877 response.headers_mut().insert(
878 header::CONTENT_RANGE,
879 HeaderValue::from_static("bytes 0-9/100"),
880 );
881 response
882 },
883 false,
884 ),
885 ];
886 for (name, respond, compressed) in cases {
887 let original = respond();
888 let original_encoding = original.headers().get(header::CONTENT_ENCODING).cloned();
889 let original_body = axum::body::to_bytes(original.into_body(), usize::MAX)
890 .await
891 .unwrap();
892 let response = through_middleware("br, gzip", "br", respond).await;
893 let encoding = response.headers().get(header::CONTENT_ENCODING).cloned();
894 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
895 .await
896 .unwrap();
897 if compressed {
898 assert_eq!(encoding, Some(Encoding::Brotli.header_value()), "{name}");
899 assert_eq!(decode(Encoding::Brotli, &body), original_body, "{name}");
900 } else {
901 assert_eq!(encoding, original_encoding, "{name}");
902 assert_eq!(body, original_body, "{name}");
903 }
904 }
905 }
906
907 #[tokio::test]
908 async fn dynamic_compression_answers_406_when_identity_is_refused() {
909 let response = through_middleware("br, gzip", "zstd, identity;q=0", || {
910 html(Body::from(page(50)))
911 })
912 .await;
913 assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE);
914 assert_eq!(response.headers()[header::VARY], "accept-encoding");
915 assert!(!response.headers().contains_key(header::CONTENT_ENCODING));
916 }
917
918 #[tokio::test]
920 async fn dynamic_compression_flushes_when_upstream_pauses() {
921 use std::sync::{Arc, Mutex};
922 use tower::ServiceExt;
923
924 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, io::Error>>(4);
925 let rx = Arc::new(Mutex::new(Some(rx)));
926 let algorithms = parse_compression_algorithms("br, gzip").unwrap();
927 let app = axum::Router::new()
928 .route(
929 "/",
930 axum::routing::get(move || {
931 let rx = rx.lock().unwrap().take().unwrap();
932 async move {
933 html(Body::from_stream(
934 tokio_stream::wrappers::ReceiverStream::new(rx),
935 ))
936 }
937 }),
938 )
939 .layer(axum::middleware::from_fn(
940 move |req: axum::extract::Request, next: axum::middleware::Next| {
941 compress(algorithms, req, next)
942 },
943 ));
944 let (head, tail) = (page(100), page(300));
945 tx.send(Ok(Bytes::from(head.clone()))).await.unwrap();
946 let response = app
947 .oneshot(
948 axum::http::Request::get("/")
949 .header(header::ACCEPT_ENCODING, "br")
950 .body(Body::empty())
951 .unwrap(),
952 )
953 .await
954 .unwrap();
955 let mut body = response.into_body().into_data_stream();
956 let first = tokio::time::timeout(std::time::Duration::from_secs(5), body.next())
957 .await
958 .expect("上游暂停时应先送出前段")
959 .unwrap()
960 .unwrap();
961 assert!(!first.is_empty());
962
963 tx.send(Ok(Bytes::from(tail.clone()))).await.unwrap();
964 drop(tx);
965 let mut encoded = first.to_vec();
966 while let Some(chunk) = body.next().await {
967 encoded.extend_from_slice(&chunk.unwrap());
968 }
969 assert_eq!(
970 decode(Encoding::Brotli, &encoded),
971 format!("{head}{tail}").into_bytes()
972 );
973 }
974}