Rust in Everything
基于 pingora-core 0.9.0

响应带 Vary 却没压缩

主线位置:请求主线第五步。process_request 先调 early_request_filter,再让下游模块看请求头,然后才是 request_filter;响应头回来时下游模块再看一次,决定压不压。模块和压缩的机制见 HTTP 模块与压缩。

sequenceDiagram
  participant H as 钩子
  participant M as 压缩模块
  H->>M: early_request_filter
  Note over M: request_header_filter 级别 0 不记 AE
  H->>M: request_filter 里 adjust_level 6
  Note over M: 响应头 已启用 加 Vary
  Note over M: AE 是空的 不压

实验 crate exp-pingora/pitb-lab,bin p22_gzip,脚本 tools/p22.sh,上游 tools/origin.py(HTTP/1.1,/page.html 是 4227 字节的 text/html,带 Content-Length)(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。代理在 19370,上游在 19371。

现象

没有覆盖 init_downstream_modules,用默认加进来的压缩模块,在 request_filter 里打开它:

fn gzip_on(session: &mut Session) {
  if let Some(c) = session.downstream_modules_ctx.get_mut::<ResponseCompression>() {
    c.adjust_level(6);
  }
}

async fn request_filter(&self, session: &mut Session, _: &mut ()) -> Result<bool> {
  gzip_on(session);
  Ok(false)
}

curl -D - -H 'Accept-Encoding: gzip'(Date、Last-Modified、Server 三行滤掉了):

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 4227
vary: accept-encoding
Connection: keep-alive

body bytes on wire: 4227

没压缩,4227 字节原样过来,却多了一个 vary: accept-encoding。浏览器和下游缓存会因此按 Accept-Encoding 分开存同一份未压缩的内容。代码不报错,日志里什么都没有。

出处

process_request 里三步的顺序:

        if let Err(e) = self
            .inner
            .early_request_filter(&mut session, &mut ctx)
            .await
        {
            // ...
        }
        // ...
        let req = session.downstream_session.req_header_mut();

        // Built-in downstream request filters go first
        if let Err(e) = session
            .downstream_modules_ctx
            .request_header_filter(req)
            .await
        {
            // ...
        }

        match self.inner.request_filter(&mut session, &mut ctx).await {

压缩模块的 request_header_filter 最终调 ResponseCompressionCtx::request_filter,它第一行是 if !self.is_enabled() { return; }:级别还是 0,Accept-Encoding 没有被记下来。等 request_filter 把级别调成 6,响应头到达时 response_header_filter 看到「已启用」,只要响应可能因 Accept-Encoding 而不同就加 Vary;可它手里的 accept_encoding 是空的,decide_action 走 Noop。默认的 init_downstream_modules 加的就是 ResponseCompressionBuilder::enable(0),注释是「Add disabled downstream compression module by default」。

📝NOTE
冷知识:0.3.0 之前,在 request_filter 里打开压缩是对的

0.3.0 之前没有下游模块,下游压缩是 Session 上的一个字段 downstream_compression: ResponseCompressionCtx。process_request 先调用户的 request_filter,之后才是一行注释 // all built-in downstream request filters go below 和 session.downstream_compression.request_filter(...),所以在 request_filter 里调级别,压缩上下文读请求头时已经启用了。提交 11863d2(「Introduce HttpModules to pingora-proxy.」,没有 PR 编号,首次进入 0.3.0)把下游压缩改成模块,并把模块的请求头过滤挪到 request_filter 之前(注释变成 // Built-in downstream request filters go first),同时新加了 early_request_filter,说明里写的是「Add: a new early_request_filter API to fine tune the behavior of the modules.」。CHANGELOG 0.3.0 只写了「Add support for HTTP modules」和「Rework the API of HTTP modules」,没有提这个顺序变化。照 0.2 时代的写法升级上来的代码,就是上面这个样子。

修法

把同一行挪到 early_request_filter:

async fn early_request_filter(&self, session: &mut Session, _: &mut ()) -> Result<()> {
  gzip_on(session);
  Ok(())
}

MODE=fix:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Encoding: gzip
vary: accept-encoding
Transfer-Encoding: chunked
Connection: keep-alive

body bytes on wire: 118

4227 字节压成 118 字节(页面是同一句话重复 140 次,压缩率特别高);Content-Length 换成了 chunked,因为边读边压不知道总长。pingora 自己的测试(pingora-proxy/tests/utils/server_utils.rs)也是在 early_request_filter 里调 adjust_level(6)。所有请求都要压缩时,更直接的是覆盖 init_downstream_modules,加一个级别不为 0 的模块:

fn init_downstream_modules(&self, modules: &mut HttpModules) {
  modules.add_module(ResponseCompressionBuilder::enable(6));
}

MODE=init(不在任何钩子里调级别)得到同样的 Content-Encoding: gzip 和 118 字节。

early_request_filter 在所有下游模块之前运行,trait 文档提醒:做访问控制和限流的模块也在它之后,所以鉴权之类的逻辑仍应留在 request_filter。

怎么确认没中招

  • 搜 adjust_level / adjust_algorithm_level / adjust_decompression:调用处在 request_filter 或更晚的钩子里,下游压缩就不会生效。session.upstream_compression 不是模块,不受这个顺序影响。
  • 对一个大于 20 字节的 text/* 响应带 Accept-Encoding: gzip 请求:有 vary: accept-encoding 却没有 Content-Encoding,就是这个坑。
  • 从 0.2.x 升级上来的代码,逐个检查原来在 request_filter 里改 downstream_compression 的地方。

小结

  • 下游模块在 early_request_filter 之后、request_filter 之前读请求头;默认的压缩模块级别是 0,此时不记 Accept-Encoding。
  • 在 request_filter 里打开压缩:响应加了 Vary 却不压缩,没有任何报错。0.3.0(11863d2)之前这样写是对的。
  • 修法:放进 early_request_filter,或者在 init_downstream_modules 里直接给一个非 0 级别。

💬 关联讨论

发起讨论
加载中...