Skip to main content

yggdrasil/
compression.rs

1//! HTTP 响应压缩:`COMPRESSION_ALGORITHMS` 配置、`Accept-Encoding` 协商、编码器与动态压缩中间件。
2//!
3//! 协商与跳过规则沿用 tower-http 0.7 的 `CompressionLayer`。动态响应由 [`compress`] 现压;
4//! SSR HTML 缓存命中时按同一协商挑选预压缩变体(`ssr_cache`)。
5
6#![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/// 压缩算法配置。
24#[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    /// 已启用的压缩编码(不含 identity),按偏好从低到高。
57    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
64/// 解析 COMPRESSION_ALGORITHMS 环境变量值。
65/// ""、"none"、"off" 返回 None;"all" 或未识别到任何算法时启用全部。
66pub(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
114/// 进程内生效的压缩配置(默认关闭);进程启动后不变,改环境变量需重启。
115pub(crate) fn algorithms() -> Option<CompressionAlgorithms> {
116    *ALGORITHMS
117}
118
119/// 内容编码,变体按偏好从低到高排列:同 q 值时 zstd > br > gzip > deflate > identity。
120#[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
161/// RFC 9110 §5.3.1 的 q 值,按千分比存为 0..=1000;格式不合法返回 None。
162fn 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
184/// 按 `Accept-Encoding` 选出响应编码;返回 None 表示客户端连 identity 都拒绝(应回 406)。
185///
186/// 与 tower-http 0.7 相同:未列出的编码忽略;没有 `*` 时只看显式列出的编码,全为 q=0
187/// 则回退 identity(除非 `identity;q=0`);有 `*` 时未列出的已启用编码取通配 q 值。
188pub(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/// 压缩力度。
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub(crate) enum Effort {
246    /// 每个请求现压:统一质量 7。brotli 4 只比 gzip-6 小 2–4%;取 7 时 brotli 比 6 再小
247    /// 1.9–2.9%,每次只多几毫秒;q9 起 feed 压缩耗时成倍增加。
248    Dynamic,
249    /// 一次压缩、多次复用(SSR HTML 缓存):取各算法高档,brotli 11 比 7 再小约 8%。
250    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
264/// brotli 默认窗口 4 MiB(lgwin 22)。流式编码不知道总长时环形缓冲区按窗口整块分配并清零,
265/// 超过约 64 KiB 的页面每次都要清 4 MiB;长度已知时取刚好容纳整段输入的窗口,压缩结果不变。
266/// 下限 18:q7 在 lgwin 16 时换用另一种哈希,33 KB 的首页反而慢 4 倍(0.54 → 2.2 ms)。
267fn brotli_window(len: Option<usize>) -> i32 {
268    const MIN: i32 = 18;
269    const MAX: i32 = 22;
270    // 窗口实际容量为 2^lgwin - 16 字节。
271    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
277/// 同步编码器:增量写入,`finish` 收尾;输出追加到调用方的 Vec。
278pub(crate) struct Encoder {
279    codec: Box<dyn EncodeV2 + Send>,
280    scratch: Box<[u8]>,
281}
282
283impl Encoder {
284    /// `len` 为已知的输入总长,用于收窄 brotli 窗口;identity 返回 None。
285    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    /// 把已写入的输入全部压出,输出可被客户端立即解码(流式响应的前段)。
315    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
338/// 一次性压缩整段输入;identity 返回 None。
339pub(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
354/// 动态压缩的缓冲上限:长度未知的正文在此之内结束时按总长收窄 brotli 窗口,超过后转流式。
355const BUFFER_LIMIT: usize = 1024 * 1024;
356
357/// 动态响应压缩中间件(`axum::middleware::from_fn`)。跳过规则同 tower-http 的 `DefaultPredicate`:
358/// - 已带 `Content-Encoding`(public 下预压缩的 `.br`、SSR 缓存的预压缩变体)或 `Content-Range` 的原样透传;
359/// - 跳过 `image/*`(`image/svg+xml` 除外)、gRPC、`text/event-stream` 与小于 32 字节的响应;
360/// - 客户端连 identity 都拒绝时回 406。
361///
362/// 与 tower-http 不同的一步:长度未知的正文先缓冲,若上游一口气给完且不超过 [`BUFFER_LIMIT`],
363/// 就按总长选 brotli 窗口([`brotli_window`]),省掉默认 4 MiB 环形缓冲区的分配与清零。
364/// 上游暂时没有数据(流式 SSR 等后续分块)时与 tower-http 一样先 flush 已收到的部分,
365/// 此后按 4 MiB 窗口流式压缩,前段照旧尽早到达浏览器。
366pub(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    // 下游在流结束后可能再次 poll;unfold 对此会 panic。
411    .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    /// 长度未知:攒到 [`BUFFER_LIMIT`]、上游暂停或正文结束。
461    Buffering(Vec<u8>),
462    /// `pending`:上次 flush 之后又写入过数据。
463    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    /// 下一段非空压缩输出;None 表示结束。上游或编码出错后结束。
478    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                    // 上游暂停:先送出已收到的部分,再等后续分块。
487                    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    /// 转入流式压缩:总长未知,按默认窗口。
517    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        // 不合法的 q 值整项忽略。
708        assert_eq!(
709            pick("br, gzip", "br;q=2, gzip;q=0.1234"),
710            Some(Encoding::Identity)
711        );
712        // 只拒绝压缩时仍可回 identity;连 identity 都拒绝则 406。
713        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    /// 测试响应工厂:每次调用产出一份新响应(`Response` 不能克隆)。
754    type Respond = fn() -> Response;
755
756    /// 经 [`compress`] 请求一个由 `respond` 产出的响应。
757    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    /// 小页面:长度已知 / 未知(分块);大页面:超过缓冲上限,转流式。
796    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    /// 流式响应(如流式 SSR)上游暂停时先送出已收到部分,不等正文结束。
919    #[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}