
同一条 / 请求,客户端只改 Accept-Encoding,服务端就能分别回明文和 gzip。响应里的 Vary: Accept-Encoding 说明表示选择依赖这个请求头。这个小实验验证了服务端怎么选、响应怎么写,没启动缓存。
两种请求,两份响应
本机用 Node 建一个临时 HTTP 服务,地址绑定到 127.0.0.1 的随机端口。客户端通过 http.request 连两次,分别发送 identity 与 gzip。服务端只按请求头里的 gzip 项决定是否压缩,其他协商细节不在这个例子里处理。
明文响应没有 Content-Encoding;压缩响应带 Content-Encoding: gzip。两种响应都带同一个 Vary: Accept-Encoding,响应体解压后内容相同。请求和响应的实测结果如下:
Node v26.8.1, local server 127.0.0.1:54013
same URL, two explicit Accept-Encoding request values
request Accept-Encoding: identity
response: 200 Vary=Accept-Encoding Content-Encoding=(absent)
body: 11 raw bytes, decoded="hello vary\n"
request Accept-Encoding: gzip
response: 200 Vary=Accept-Encoding Content-Encoding=gzip
body: 31 raw bytes, decoded="hello vary\n"
scope: server/client request-response behavior only; no cache was used
这里的 Vary 是响应选择的说明,不是缓存操作指令。服务器声明内容可能随着 Accept-Encoding 不同而变化。按照 HTTP 缓存规则,缓存需要把列出的请求头纳入响应变体的匹配;但这段程序没有实现缓存,所以输出不能证明某个缓存一定照做。
为什么只看两个响应不够
若只观察响应体,容易把这件事说成「gzip 开关生效」。实验确实验证了压缩开关,不过额外验证到的是两条响应都公开写出 Vary,且解码后的正文一致。这个头告诉后续缓存,单看 URL 和方法不足以判断两份响应是否可互换。
边界也很具体。脚本没有复用缓存条目,没有记录缓存命中或未命中,也没有放浏览器和代理参与。实际缓存的键如何处理字段、何时重新验证,还取决于具体实现与配置。这次本机收发不能充当代理缓存的证据。
还有个容易混淆的点:请求里出现 Accept-Encoding: gzip,不代表客户端一定能处理任意压缩格式;例子只明确发了 gzip,并且客户端知道何时解压。相反,identity 分支的响应没有 Content-Encoding,这次收发确认的是服务端确实走了另一条表示路径。它没有去比较压缩率,也没测内容更大时压缩是否值得。
响应头里的 Vary 字段值是大小写不敏感的 HTTP 字段名,这里恰好只有 Accept-Encoding。若要声明多个选择条件,值可以列出多个字段名,但这个小服务刻意只做一个维度,避免把请求头解析、压缩质量权重等问题混在一起。它只识别逗号分隔后名称严格等于 gzip 的项,没实现完整的内容协商算法。比如 gzip;q=0 仍会被这个简化判断当成支持 gzip,因为脚本没有读取质量权重。真实服务端要处理完整请求语法,不能直接照搬这几行判断。
Accept-Encoding 也可以携带多个编码和质量值,协议语义比这个开关复杂得多。这里故意把输入压成两种明确值,避免把字段解析的边角行为误写成缓存行为。验证点只有一个:服务端能根据请求头选择响应表示,并在响应中声明该字段会影响选择。
Vary 不会替服务端协商
响应头不会替服务器自动压缩内容。上面的服务端代码显式读取 Accept-Encoding,再生成 gzip 或原始字节;Vary 只把这一选择条件写进响应元数据。如果服务端始终返回同一种表示,却随手加上 Vary,只会让缓存按一个没有实际影响的维度区分响应。
同理,Vary 也不意味着请求必定会进入共享缓存。响应可能因其他缓存指令而不可存,客户端也可能根本不缓存。要验证缓存是否正确,必须另外搭出能观察命中行为的缓存层,并针对不同请求头重复读取同一 URL。
我倾向于把这次结果只记作协议收发事实:Accept-Encoding 不同,Node 服务器返回的编码不同,Vary 头在两个响应里都出现。它不能替浏览器或代理背书。需要确认具体缓存实现时,应直接测那一层,而不是从一个正确的响应头推断缓存行为。