响应带 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」。
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 级别。