Skip to content

fix(web): * 不得覆盖 Accept-Encoding 里显式给出的 q=0 - #10

Merged
Antruly merged 1 commit into
Antruly:masterfrom
sercebr:fix/compression-head-and-qvalue
Sep 19, 2026
Merged

Antruly merged 1 commit into
Antruly:masterfrom
sercebr:fix/compression-head-and-qvalue

Conversation

@sercebr

@sercebr sercebr commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

parse_accept_encoding 对 q=0 的处理是 if (q == 0.0) continue; —— 只是跳过这一项,没有记下"这个编码被显式拒了"。于是后面 * 那一支 method = GZIP 又把它选回来,而且 * 的 q 默认 1.0,优先级还最高。

RFC 9110 §12.5.3:

The asterisk "*" symbol in an Accept-Encoding field matches any available
content encoding not explicitly listed in the field.

也就是说 * 管不着显式列出的编码。现状是客户端发一个"gzip 我收不了",服务端回 gzip:

Accept-Encoding 修改前 应为
*, gzip;q=0 gzip deflate
gzip;q=0, * gzip deflate
*, gzip;q=0, deflate;q=0 gzip 不压
*, deflate;q=0 gzip gzip ✅

仓库里既有的用例为什么没抓到

web_http_client_func.cpp 里那条

if (http_compress::parse_accept_encoding("gzip;q=0, deflate") != http_compress_method::DEFLATE)

它成立靠的是"deflate 显式出现、且 q 更高"—— 两侧都只由 * 覆盖时才会露出来。补的 4 条正好走那条缝。

修法

  1. q=0 那一支把编码记进 gzip_ok / deflate_ok,* 选择时避开;
  2. 收尾再加一道 —— 因为上面按 q 取最大值,而 q=0 是"直接跳过",所以
    gzip;q=0, gzip;q=0.5 这种自相矛盾输入仍会把 gzip 选回来。显式拒绝优先:
    少发一层编码只是没优化,发错一层编码是客户端解不开。

验证

变异验证(同一条断言、只换 DLL,不只看时间戳 —— 比过 md5):

变异 结果
还原为 master 的 parse_accept_encoding 3 条 FAIL
本分支 10/10 PASS
5 条既有断言 两边都绿 —— 改动没碰既有行为

第 4 条用例(*, deflate;q=0 → gzip)两边都过,它不区分。留着是作为反向守卫:挡住"把 deflate 也一起拒掉"这种过度修正。写在这里免得它被当成分辨性用例。

另有端到端证据:开了压缩的靶场上,Accept-Encoding: *, gzip;q=0 的响应头从 content-encoding: gzip 变成 deflate。

一个顺带说明

同一轮还发现 HEAD 与 GET 在压缩下的头不一致(HEAD 报未压缩长度、GET 发压缩体,照 HEAD 读的客户端会挂住)。那条不在这个 PR 里 —— 根因在 webapp 层(长度在压缩发生之前就被 set_head_only / 静态层的 need_data = !is_head 钉死了),修法要动设计,我在 issue 里另开一条,想先听你的意见。

`parse_accept_encoding` 对 `q=0` 只是 `continue` 跳过,没有记下"这个编码被
显式拒了",于是 `*` 那一支又把它选回来,而且 `*` 默认 q=1.0、优先级还最高。
RFC 9110 §12.5.3 里 `*` 只匹配**没有显式列出**的编码,所以客户端发
`*, gzip;q=0` 会收到 gzip —— 一个它刚声明过收不了的编码。

修法:
  - 记下被显式拒掉的编码,`*` 选择时避开;
  - 收尾再加一道,挡住 `gzip;q=0, gzip;q=0.5` 这种自相矛盾输入把 gzip 选回来。

仓库里既有的 `"gzip;q=0, deflate"` 用例抓不到这条:它靠的是 deflate 显式出现
且 q 更高;只有两侧都仅由 `*` 覆盖时才露出来。已补 4 条用例。

变异验证(同一条断言、只换 DLL):
  未修正 -> 3 条 FAIL(第 4 条 `*, deflate;q=0` 两边都过,它不区分,作用是
                    挡住"把 deflate 也一起拒掉"这种过度修正)
  修正后 -> 10/10 PASS,5 条既有断言两边都绿(未碰既有行为)
@Antruly

Antruly commented Sep 19, 2026

Copy link
Copy Markdown
Owner

核过了,* 那一半是对的,既有那条用例为什么抓不到也说得对。我按你的表在本机
A/B 跑了一遍(同一个纯函数,只换 uvcpp_http_compress.cpp 编出来的 DLL/可执行,
-DUVCPP_ZLIB_ENABLE=0 把那一段整个编译掉,所以与 zlib 无关):

Accept-Encoding master 本分支
*, gzip;q=0 GZIP ❌ DEFLATE
gzip;q=0, * GZIP ❌ DEFLATE
*, gzip;q=0, deflate;q=0 GZIP ❌ NONE
*, deflate;q=0 GZIP GZIP(不区分,同你所注)
gzip;q=0, deflate DEFLATE DEFLATE

但收尾那一道会把同一类错误换个形状带回来,而且正好落在你写下的那条原则上
(「少发一层编码只是没优化,发错一层编码是客户端解不开」):

gzip;q=0, gzip;q=0.5        -> DEFLATE     ← 客户端只列了 gzip,从没说过收 deflate
deflate;q=0, deflate;q=0.5  -> GZIP        ← 同上,反过来

没有 * 的时候,未被列出的编码默认不可接受(RFC 9110 §12.5.3),而
deflate_ok / gzip_ok 的初值是 true —— 它的含义是「没被显式拒过」,不等于
「被接受了」。收尾那道现在拿它当后者的判据,于是给一个只列了 gzip 的客户端发
content-encoding: deflate。

(gzip;q=0, gzip;q=0.5 本身是自相矛盾的输入,怎么定都不算错;但"退回一个
压根没被接受的编码"是——这条正是本 PR 要修的那一类。)

收紧的办法:收尾那道只退回到本身可接受的编码,也就是"显式列出且 q>0"
或"被 * 覆盖到"的;两个都不满足就 NONE。现在的 gzip_ok/deflate_ok
再各配一个"见过且 q>0"的标志就够(* 那一支已经在选择时把 ok 折进去了,所以
* 覆盖的情况天然带上)。

如果你改,我这边等这个 PR 更新;不想再动它的话说一声,我合进去之后自己补一条
follow-up 把它收紧——两种都行,你挑。

其余部分我这边没有别的意见,CI 也是绿的。

@Antruly
Antruly merged commit 1a473af into Antruly:master Sep 19, 2026
16 checks passed
Antruly added a commit that referenced this pull request Sep 19, 2026
`*_ok` 的语义是"没被 `q=0` 显式拒过",**不等于**"被接受了":没有 `*` 的时候,
未被列出的编码默认不可接受(RFC 9110 §12.5.3)。拿前者当后者用,就会给一个
只列了 gzip 的客户端发 `content-encoding: deflate` —— #10 修掉的那类错误换个
形状又回来了(`gzip;q=0, gzip;q=0.5` 会被选回 gzip)。

收尾判据改成一条:**退回的目标必须本身可接受** —— "显式列出且 q>0" 或
"被 `*` 覆盖到",两个都不满足就是 `NONE`。顺带把 `*;q=0` 带对了:它原来是被
整条跳过,按 RFC 应当读作"未列出的一律不可接受"。

Co-Authored-By: Claude Code <noreply@anthropic.com>
Antruly added a commit that referenced this pull request Sep 19, 2026
新增五条:自相矛盾的显式拒绝(`gzip;q=0, gzip;q=0.5`)、只拒一个、
`*;q=0`、`*;q=0` 加显式 gzip。

两个方向的变异都实测撞红,不是推理:
- 收尾退回 `gzip_ok` 单独用(= #10 那一版的语义)→ accept_encoding FAIL;
- 收紧过头成 `gzip_ok && wildcard` → 函数开头那三条反向守卫(`gzip`/`*`/`deflate`
  照常选中)FAIL,所以反向守卫不在这里重复。

Co-Authored-By: Claude Code <noreply@anthropic.com>
Antruly added a commit that referenced this pull request Sep 19, 2026
Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants