Rust in Everything
基于 tower-http 0.7.1

压缩

主线位置:请求主线上的一层中间件,用 Router::layer 套上(装配时只是一次 Layer::layer)。它在请求进来时只读一个头 Accept-Encoding,然后原样交给内层;真正的工作在响应回来时:决定压不压、改响应头、把 body 换成一个边读边压的流。

flowchart TB
  req["请求"] --> neg["读 Accept-Encoding,选编码"]
  neg --> inner["调用内层服务"]
  inner --> none{"选不出编码"}
  none -- 是 --> r406["改成 406,body 原样"]
  none -- 否 --> hard{"已编码或是 Range"}
  hard -- 是 --> pass["原样返回"]
  hard -- 否 --> pred{"谓词:大小、类型"}
  pred -- 不压 --> pass
  pred -- 压 --> vary["加 Vary"]
  vary --> idn{"选中 identity"}
  idn -- 是 --> pass2["body 不变"]
  idn -- 否 --> wrap["包成压缩 body,改头"]

本页源码在 tower-http/src/compression/、compression_utils.rs 和 content_encoding.rs。示例用这一组依赖(compression-full 打开 gzip、deflate、br、zstd 四个 feature;fs 只有最后一节的 ServeDir 需要;test-util 给流式那一节的暂停时钟用):

axum = "=0.8.9"
tower-http = { version = "=0.7.1", features = ["compression-full", "fs"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "test-util"] }

所有输出都在 M2 Max、rustc 1.99.0、--release 下跑 3 次,结果相同。

结构

CompressionLayer 和它生成的 Compression 都只有三样配置:

pub struct Compression<S, P = DefaultPredicate> {
    pub(crate) inner: S,
    pub(crate) accept: AcceptEncoding,
    pub(crate) predicate: P,
    pub(crate) quality: CompressionLevel,
}
  • accept 是四个布尔值,默认全开。但编码名的解析是用 #[cfg(feature = ...)] 包着的:只开了 compression-gzip 时,br 在解析阶段就被当成未知编码丢掉。所以「支持哪些编码」由编译时的 feature 和运行时的 .gzip(false) / .no_br() 一起决定。
  • predicate 决定一个响应值不值得压,默认是 DefaultPredicate,可以用 compress_when 换掉。
  • quality 是 CompressionLevel,默认 Default,即各算法自己的默认级别,交给 async-compression 0.4.51。

call 只做一件事:在把请求交给内层之前算好编码,存进 future。

    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
        let encoding = Encoding::from_headers(req.headers(), self.accept);

        ResponseFuture {
            inner: self.inner.call(req),
            encoding,
            predicate: self.predicate.clone(),
            quality: self.quality,
        }
    }

请求一进来就要算,是因为请求在 self.inner.call(req) 里被移走了,响应回来时已经看不到 Accept-Encoding。

协商:q 值、优先级和通配符

encodings 把所有 Accept-Encoding 头按逗号拆开,每一项再按 ; 拆成编码名和 q 值:

  • 编码名不区分大小写,x-gzip 等同于 gzip;不认识的(compress)或没开 feature 的直接丢掉,* 也在这里被丢掉,由下面单独处理。
  • q 值由 QValue::parse 解析成 0–1000 的整数,只接受 0、1 开头、最多 3 位小数;格式不对的,这一项整个丢掉(不是当成 q=1)。没写 q 值就是 1000。

选择由 Encoding::preferred_encoding 完成:

        accepted_encodings
            .filter(|(_, qvalue)| qvalue.0 > 0)
            .max_by_key(|&(encoding, qvalue)| (qvalue, encoding))
            .map(|(encoding, _)| encoding)

先去掉 q=0 的,再按 (q 值, 编码) 取最大。第二个分量是 Encoding 枚举本身,它派生了 Ord,注释写着「This enum's variants are ordered from least to most preferred」:Identity < Deflate < Gzip < Brotli < Zstd。q 值相同时,排在后面的赢。

通配符和「拒绝不压缩」由 preferred_encoding_with_wildcard 处理:

    let wildcard_q = match wildcard_q {
        Some(q) => q,
        None => {
            let identity_rejected = explicit
                .iter()
                .any(|(enc, q)| *enc == Encoding::Identity && q.0 == 0);
            return match Encoding::preferred_encoding(explicit.into_iter()) {
                Some(enc) => Some(enc),
                None => {
                    if identity_rejected {
                        None
                    } else {
                        Some(Encoding::Identity)
                    }
                }
            };
        }
    };

    // Build the effective set of (encoding, qvalue) for all supported encodings.
    // For each supported encoding, use its explicit q-value if listed, otherwise the wildcard
    // q-value.
    let all_supported = all_supported_encodings(supported_encoding);
    // ...
    Encoding::preferred_encoding(effective)
  • 没有 *:从列出来的里面选;一个都选不出时,除非客户端写了 identity;q=0,否则退回 identity(不压缩)。没有 Accept-Encoding 头也走这条路,结果是 identity。
  • 有 *:所有支持的编码(包括 identity)都拿到通配符的 q 值,显式写出的编码用自己的 q 值。* 的 q 为 1 时,所有编码同分,按枚举顺序选 zstd。
  • 返回 None 表示没有可接受的编码。这时 ResponseFuture::poll 等内层返回后,只把状态码改成 406,body 和其他头原样保留(注释:「the inner service has already been called, so its response body and headers are passed through. Only the status code is overwritten」)。
use axum::{Router, body::Body, http::Request, routing::get};
use http_body_util::BodyExt;
use tower::ServiceExt;
use tower_http::compression::CompressionLayer;

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/", get(|| async { "hello ".repeat(100) }))
    .layer(CompressionLayer::new());
  for accept in [
    "",
    "gzip",
    "gzip, deflate, br, zstd",
    "gzip, br;q=0.9",
    "br;q=0, gzip;q=0.5",
    "x-gzip",
    "GZIP",
    "gzip;q=0.0001",
    "compress",
    "*",
    "*;q=0, gzip",
    "identity;q=0",
    "*;q=0",
  ] {
    let mut req = Request::get("/");
    if !accept.is_empty() {
      req = req.header("accept-encoding", accept);
    }
    let res = app.clone().oneshot(req.body(Body::empty()).unwrap()).await.unwrap();
    let status = res.status().as_u16();
    let enc = res.headers().get("content-encoding").map(|v| v.to_str().unwrap().to_owned()).unwrap_or("-".into());
    let vary = res.headers().get("vary").map(|v| v.to_str().unwrap().to_owned()).unwrap_or("-".into());
    let len = res.into_body().collect().await.unwrap().to_bytes().len();
    println!("{:<26} {status} enc={enc:<5} vary={vary:<16} body={len}", format!("{accept:?}"));
  }
}
""                         200 enc=-     vary=accept-encoding  body=600
"gzip"                     200 enc=gzip  vary=accept-encoding  body=44
"gzip, deflate, br, zstd"  200 enc=zstd  vary=accept-encoding  body=22
"gzip, br;q=0.9"           200 enc=gzip  vary=accept-encoding  body=44
"br;q=0, gzip;q=0.5"       200 enc=gzip  vary=accept-encoding  body=44
"x-gzip"                   200 enc=gzip  vary=accept-encoding  body=44
"GZIP"                     200 enc=gzip  vary=accept-encoding  body=44
"gzip;q=0.0001"            200 enc=-     vary=accept-encoding  body=600
"compress"                 200 enc=-     vary=accept-encoding  body=600
"*"                        200 enc=zstd  vary=accept-encoding  body=22
"*;q=0, gzip"              200 enc=gzip  vary=accept-encoding  body=44
"identity;q=0"             406 enc=-     vary=accept-encoding  body=600
"*;q=0"                    406 enc=-     vary=accept-encoding  body=600
  • 600 字节的重复文本,gzip 压成 44 字节,zstd 22 字节。
  • gzip, deflate, br, zstd 是常见浏览器发的那种写法,四个都是 q=1,选 zstd。
  • x-gzip 选中后,响应头写的是规范名 gzip。
  • gzip;q=0.0001:q 值四位小数,这一项被丢掉,结果和 compress、没有头一样,不压缩。
  • 406 的两行 body 还是 600 字节的原文。客户端说「什么都不接受」,中间件仍然调用了 handler,只是把状态码换掉。
  • 每一行都带 Vary,包括没压缩的,原因在下一节。

ServeDir 挑预压缩文件也用 encodings 和 preferred_encoding,但不走 preferred_encoding_with_wildcard,所以它不认 *,也不会回 406(见 ServeDir 与 ServeFile)。

📝NOTE
冷知识:* 和 406 是 0.7.0 才有的

0.7.0 之前,Encoding::from_headers 直接返回一个 Encoding,通配符被当成未知编码忽略。#693「Fix: Handle wildcard * in Accept-Encoding per RFC 9110 §12.5.3」的说明:「The compression middleware silently ignores the * wildcard in Accept-Encoding headers. This means requests like *;q=0,gzip (which per RFC 9110 §12.5.3 should select gzip and reject everything else) fall back to identity encoding instead.」

改法是让 from_headers 返回 Option<Encoding>,None 表示无法满足;为了不改中间件的类型签名,「The ResponseFuture handles the None case by overwriting the response status to 406 while passing through the inner service's body unchanged. This avoids introducing a new error type or changing the middleware's Service output type.」这就是上面「handler 照样执行、只改状态码」的来历。PR 也明说了 fs 不受影响,「since it uses preferred_encoding() directly with its own encoding negotiation logic」。CHANGELOG 把它列在 0.7.0 的 breaking 下:之前 identity;q=0 会静默拿到不压缩的响应,现在是 406。

压不压:两条硬规则和 DefaultPredicate

ResponseFuture::poll 拿到内层的响应之后:

        // never recompress responses that are already compressed
        let should_compress = !res.headers().contains_key(header::CONTENT_ENCODING)
            // never compress responses that are ranges
            && !res.headers().contains_key(header::CONTENT_RANGE)
            && self.predicate.should_compress(&res);

前两条写死在 future 里,和谓词无关,换了谓词也绕不过:已经有 Content-Encoding 的不再压(比如 ServeDir 给出的 .gz 文件),有 Content-Range 的不压(206 的那一段字节是原文件的某个区间,压缩后偏移就对不上了)。

剩下的交给谓词。DefaultPredicate 是四个谓词的 And:

        let inner = SizeAbove::new(SizeAbove::DEFAULT_MIN_SIZE)
            .and(NotForContentType::GRPC)
            .and(NotForContentType::IMAGES)
            .and(NotForContentType::SSE);
  • SizeAbove:DEFAULT_MIN_SIZE 是 32。大小先取 body 的 size_hint().exact(),没有再读 Content-Length 头;size >= 32 才压。两者都没有(流式 body)时一律压。
  • NotForContentType 用 starts_with 比较 Content-Type,可以带一个例外前缀:GRPC 是 application/grpc,例外 application/grpc-web;IMAGES 是 image/,例外 image/svg+xml(SVG 是文本);SSE 是 text/event-stream,没有例外。比较区分大小写。

gRPC 有自己的消息级压缩(文档原话「which has its own protocol specific compression scheme」),常见的图片格式本身已经压缩过。SSE 是 0.5.2 加进默认谓词的(#465),PR 的理由是「If not careful or properly handled, general compression with SSE will result in missing some or all events.」(axum 的 Sse 怎么产生事件见 SSE 与重定向。)

use axum::{
  Router,
  body::Body,
  http::{Request, header},
  response::IntoResponse,
  routing::get,
};
use http_body_util::BodyExt;
use tower::ServiceExt;
use tower_http::compression::CompressionLayer;

fn text(n: usize) -> String {
  "a".repeat(n)
}

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/31", get(|| async { text(31) }))
    .route("/32", get(|| async { text(32) }))
    .route("/html", get(|| async { ([(header::CONTENT_TYPE, "text/html")], text(2000)) }))
    .route("/png", get(|| async { ([(header::CONTENT_TYPE, "image/png")], text(2000)) }))
    .route("/svg", get(|| async { ([(header::CONTENT_TYPE, "image/svg+xml")], text(2000)) }))
    .route("/sse", get(|| async { ([(header::CONTENT_TYPE, "text/event-stream")], text(2000)) }))
    .route("/grpc", get(|| async { ([(header::CONTENT_TYPE, "application/grpc")], text(2000)) }))
    .route("/grpc-web", get(|| async { ([(header::CONTENT_TYPE, "application/grpc-web")], text(2000)) }))
    .route("/encoded", get(|| async { ([(header::CONTENT_ENCODING, "gzip")], text(2000)) }))
    .route("/ranged", get(|| async { ([(header::CONTENT_RANGE, "bytes 0-1999/5000")], text(2000)) }))
    .route(
      "/stream",
      get(|| async {
        let chunks = futures::stream::iter([Ok::<_, std::io::Error>("tiny")]);
        Body::from_stream(chunks).into_response()
      }),
    )
    .layer(CompressionLayer::new());

  println!("{:<10} {:<5} {:<5} {:<5} {}", "path", "enc", "len", "body", "vary");
  for path in ["/31", "/32", "/html", "/png", "/svg", "/sse", "/grpc", "/grpc-web", "/encoded", "/ranged", "/stream"] {
    let req = Request::get(path).header("accept-encoding", "gzip").body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let h = |name: &str| res.headers().get(name).map(|v| v.to_str().unwrap().to_owned()).unwrap_or("-".into());
    let (enc, len, vary) = (h("content-encoding"), h("content-length"), h("vary"));
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{path:<10} {enc:<5} {len:<5} {:<5} {vary}", body.len());
  }
}
path       enc   len   body  vary
/31        -     31    31    -
/32        gzip  -     23    accept-encoding
/html      gzip  -     38    accept-encoding
/png       -     2000  2000  -
/svg       gzip  -     38    accept-encoding
/sse       -     2000  2000  -
/grpc      -     2000  2000  -
/grpc-web  gzip  -     38    accept-encoding
/encoded   gzip  2000  2000  -
/ranged    -     2000  2000  -
/stream    gzip  -     24    accept-encoding
  • 31 字节不压,32 字节压,边界是 >=。32 个 a 压完是 23 字节,省得不多;/stream 只有 4 字节,因为大小未知照样被压,结果是 24 字节,比原文还大。
  • /encoded 是 handler 自己声称 gzip 的 2000 字节明文,中间件信了它,原样放过。
  • 没被压的响应都还带着 Content-Length。这个头不是 handler 写的,是 axum 的 RouteFuture 按 body 的 size_hint 补的(见 Route 与类型擦除)。

想改规则,用 compress_when 组合自己的谓词,比如 SizeAbove::new(256).and(NotForContentType::GRPC).and(NotForContentType::IMAGES)。谓词也可以是闭包 Fn(StatusCode, Version, &HeaderMap, &Extensions) -> bool。不管怎么换,前面两条硬规则都还在。

改了哪些响应头

决定压缩之后(should_compress 为真),future 先补 Vary,再看选中的编码:

        if should_compress
            && !parts.headers.get_all(header::VARY).iter().any(|value| {
                contains_ignore_ascii_case(
                    value.as_bytes(),
                    header::ACCEPT_ENCODING.as_str().as_bytes(),
                )
            })
        {
            parts
                .headers
                .append(header::VARY, header::ACCEPT_ENCODING.into());
        }

        let body = match (should_compress, encoding) {
            // if compression is _not_ supported or the client doesn't accept it
            (false, _) | (_, Encoding::Identity) => {
                return Poll::Ready(Ok(Response::from_parts(
                    parts,
                    CompressionBody::new(BodyInner::identity(body)),
                )))
            }
            // ... gzip、deflate、br、zstd 各一个分支,用 WrapBody 包住 body
        };

        parts.headers.remove(header::ACCEPT_RANGES);
        parts.headers.remove(header::CONTENT_LENGTH);

        parts
            .headers
            .insert(header::CONTENT_ENCODING, encoding.into_header_value());
  • Vary: accept-encoding 只取决于「这个响应值得压」,与这次选没选中编码无关。所以上一节没带 Accept-Encoding 的请求也拿到了 Vary:同一个 URL,换个请求头就会得到不同的字节,缓存必须知道这一点。已经有 Vary 且包含 accept-encoding(不区分大小写的子串匹配)时不重复加,用的是 append,原有的 Vary 值不受影响。
  • 真正压缩时,去掉 Content-Length(压缩后的长度事先不知道,HTTP/1.1 下 hyper 会改用 chunked 编码)和 Accept-Ranges(压缩后的字节流不能按原文件的偏移取区间),加上 Content-Encoding。

把 ServeDir 放在 CompressionLayer 里面,能同时看到这几条:

use axum::{Router, body::Body, http::Request};
use tower::ServiceExt;
use tower_http::{compression::CompressionLayer, services::ServeDir};

#[tokio::main]
async fn main() {
  std::fs::create_dir_all("tmp/cmp").unwrap();
  std::fs::write("tmp/cmp/a.txt", "hello ".repeat(100)).unwrap();
  std::fs::write("tmp/cmp/a.txt.gz", "pretend this is gzip").unwrap();
  let plain = Router::new().fallback_service(ServeDir::new("tmp/cmp"));
  let pre = Router::new().fallback_service(ServeDir::new("tmp/cmp").precompressed_gzip());
  let cases = [
    ("ServeDir", plain.clone(), None),
    ("ServeDir + Compression", plain.clone().layer(CompressionLayer::new()), None),
    ("ServeDir + Compression", plain.layer(CompressionLayer::new()), Some("bytes=0-9")),
    ("precompressed + Compression", pre.layer(CompressionLayer::new()), None),
  ];
  for (name, app, range) in cases {
    let mut req = Request::get("/a.txt").header("accept-encoding", "gzip");
    if let Some(r) = range {
      req = req.header("range", r);
    }
    let res = app.oneshot(req.body(Body::empty()).unwrap()).await.unwrap();
    let h = |name: &str| res.headers().get(name).map(|v| v.to_str().unwrap().to_owned()).unwrap_or("-".into());
    println!(
      "{name:<28} range={:<11} {} enc={:<4} len={:<3} accept-ranges={:<5} vary={}",
      range.unwrap_or("-"),
      res.status().as_u16(),
      h("content-encoding"),
      h("content-length"),
      h("accept-ranges"),
      h("vary"),
    );
  }
}
ServeDir                     range=-           200 enc=-    len=600 accept-ranges=bytes vary=-
ServeDir + Compression       range=-           200 enc=gzip len=-   accept-ranges=-     vary=accept-encoding
ServeDir + Compression       range=bytes=0-9   206 enc=-    len=10  accept-ranges=bytes vary=-
precompressed + Compression  range=-           200 enc=gzip len=20  accept-ranges=bytes vary=accept-encoding
  • 第二行:压缩后 Content-Length 和 Accept-Ranges 都没了,客户端从这个响应里看不出可以发 Range。
  • 第三行:客户端照样发了 Range,ServeDir 回 206 带 Content-Range,硬规则让它原样通过。
  • 第四行:ServeDir 选了 .gz 文件并写了 Content-Encoding,中间件不再压,Content-Length 和 Accept-Ranges 都保留;Vary 是 ServeDir 自己加的。静态文件提前压好再配 precompressed_gzip(),就省掉了每个请求的 CPU。
❗IMPORTANT
动脑筋:客户端更想要原文
use axum::{Router, body::Body, http::Request, routing::get};
use http_body_util::BodyExt;
use tower::ServiceExt;
use tower_http::compression::CompressionLayer;

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/", get(|| async { "hello ".repeat(100) }))
    .layer(CompressionLayer::new());
  let req = Request::get("/")
    .header("accept-encoding", "gzip;q=0.8, identity")
    .body(Body::empty())
    .unwrap();
  let res = app.oneshot(req).await.unwrap();
  let enc = res.headers().get("content-encoding").cloned();
  let len = res.into_body().collect().await.unwrap().to_bytes().len();
  println!("content-encoding: {enc:?}, {len} bytes");
}

客户端接受 gzip,但给它的 q 值比 identity 低。打印的是 Some("gzip") 还是 None,body 多少字节?

流式压缩

压缩 body 是 WrapBody:内层 body 先变成 Stream(BodyIntoStream),再变成 AsyncRead(tokio-util 的 StreamReader),套上 async-compression 的编码器(比如 GzipEncoder),最后在 poll_frame 里用一块初始容量 4096 字节(INTERNAL_BUF_CAPACITY)的缓冲读编码器的输出,读到多少就作为一帧交出去。整个 body 不会被攒在内存里。

编码器什么时候吐数据,决定了流式响应的延迟。async-compression 0.4.51 的 bufread 编码器在上游返回 Pending、而这之前已经读进过数据时,会对压缩器做一次 flush(generic/bufread/encoder.rs 里 State::Encoding 遇到 None 输入转成 State::Flushing)。也就是说:内层一时没有新数据,已经收到的部分就先压出来交给客户端。下面的 body 每 100 毫秒产生一块 100 字节(暂停时钟,毫秒数是虚拟时间):

use axum::{Router, body::Body, http::Request, routing::get};
use futures::StreamExt;
use http_body_util::BodyExt;
use std::time::Duration;
use tokio::time::Instant;
use tower::ServiceExt;
use tower_http::compression::CompressionLayer;

// one 100-byte chunk every 100 ms, three times
fn ticks() -> impl futures::Stream<Item = Result<String, std::io::Error>> + Send {
  futures::stream::unfold(0, |i| async move {
    if i == 3 {
      return None;
    }
    tokio::time::sleep(Duration::from_millis(100)).await;
    Some((Ok(format!("{i}").repeat(100)), i + 1))
  })
}

async fn run(accept: &'static str, fused: bool) {
  let app = Router::new()
    .route(
      "/",
      get(move || async move {
        if fused { Body::from_stream(ticks().fuse()) } else { Body::from_stream(ticks()) }
      }),
    )
    .layer(CompressionLayer::new());
  let req = Request::get("/").header("accept-encoding", accept).body(Body::empty()).unwrap();
  let start = Instant::now();
  let mut body = app.oneshot(req).await.unwrap().into_body();
  while let Some(frame) = body.frame().await {
    let len = frame.unwrap().into_data().unwrap().len();
    println!("{:>4} ms  {len} bytes", start.elapsed().as_millis());
  }
  println!("{:>4} ms  end", start.elapsed().as_millis());
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  std::panic::set_hook(Box::new(|_| {}));
  println!("-- identity");
  run("identity", false).await;
  println!("-- gzip, unfold().fuse()");
  run("gzip", true).await;
  println!("-- gzip, unfold()");
  let err = tokio::spawn(run("gzip", false)).await.unwrap_err();
  let msg = err.into_panic();
  println!("panicked: {}", msg.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| msg.downcast_ref::<String>().cloned()).unwrap());
}
-- identity
 100 ms  100 bytes
 200 ms  100 bytes
 300 ms  100 bytes
 300 ms  end
-- gzip, unfold().fuse()
 100 ms  30 bytes
 200 ms  20 bytes
 300 ms  24 bytes
 300 ms  end
-- gzip, unfold()
 100 ms  30 bytes
 200 ms  20 bytes
 300 ms  24 bytes
panicked: Unfold must not be polled after it returned `Poll::Ready(None)`
  • 压缩后每一块仍在它产生的那一刻交出,没有攒到结尾。第一帧 30 字节里有 gzip 的 10 字节头;最后一帧 24 字节里有 8 字节的 gzip 尾(CRC32 和长度)。代价是每次 flush 都让压缩率变差,小块越多越明显。
  • 第三段是一个已知问题:body 结束时程序 panic 了。WrapBody 读完编码器的输出后,还要再 poll 一次内层 body,取可能跟在数据后面的 trailers;这一次落到 BodyIntoStream::poll_frame 最后的 this.body.poll_frame(cx),而内层 body 之前已经返回过 None。futures::stream::unfold 被 poll 过结束之后会 panic,Body::from_stream 原样把这次 poll 转给它。stream::iter 这类结束后能一直返回 None 的流不受影响(上一节的 /stream 就没事);给流加 .fuse() 可以绕开。这个问题已经报告为 issue #732「CompressionLayer re-polls non-fused streaming bodies after EOF and panics」,报告者在 0.7.0 和 0.7.1 上复现,截至 0.7.1 仍未修复。在真实服务器上,这个 panic 发生在 hyper 驱动连接的任务里。

动脑筋答案

content-encoding: None, 600 bytes

跑 3 次相同。没有压缩。identity 没写 q 值,按 1000 算;gzip 是 800。preferred_encoding 比较的是 (q 值, 编码),q 值先比,(1000, Identity) 大于 (800, Gzip),枚举里 identity 排最低这件事只在 q 值相同时才起作用。选中 identity 后,响应走 (_, Encoding::Identity) 分支原样返回,但因为谓词认为它值得压,仍然带着 Vary: accept-encoding。

小结

  • CompressionLayer 在 call 时就从 Accept-Encoding 选好编码,响应回来再决定压不压。
  • 选编码:去掉 q=0,按 (q 值, 编码) 取最大,同分时 zstd > br > gzip > deflate > identity;q 值写错的项被丢掉;0.7.0 起认 *,拒绝了一切(identity;q=0、*;q=0)就把状态码改成 406,body 原样。
  • 不压的情况:已有 Content-Encoding、有 Content-Range(这两条写死);DefaultPredicate 再排除小于 32 字节、image/(SVG 除外)、application/grpc(gRPC-Web 除外)、text/event-stream。大小未知的流式 body 一律压。
  • 值得压就加 Vary: accept-encoding;真的压了就去掉 Content-Length、Accept-Ranges,写上 Content-Encoding。
  • 压缩是流式的,上游暂时没数据时编码器会 flush;0.7.1 的 WrapBody 会在 body 结束后多 poll 一次内层,未 fuse 的 unfold 流会 panic(#732)。

压缩、解压和 ServeDir 预压缩共用的编码内核见 tower-http 卷 共享内核,CompressionBody 的内部见 压缩的内部;条件加层时 CompressionBody 带来的类型问题见其中的 body 模块。

💬 关联讨论

发起讨论
加载中...