网站开启 Brotli 或 Gzip 后,可能出现一批很容易被误判的故障:部分访客页面空白,CSS、JavaScript 下载失败,浏览器提示 ERR_CONTENT_DECODING_FAILED,同一个 URL 有时正常、有时返回乱码,绕过 CDN 后却完全正常。
这类问题多半来自响应头、响应体和缓存变体没有对应起来,和浏览器是否支持压缩不是一回事。排查时要把浏览器、CDN 和源站拆开,分别确认请求接受什么编码、实际返回什么内容、共享缓存是否按编码保存了不同版本。

先保留现场,不要急着反复开关压缩
先找一个能够稳定复现的静态资源,优先选择体积较大的 CSS、JavaScript、JSON 或 HTML。图片、视频、ZIP 等文件通常已经压缩,再次启用 Brotli 或 Gzip 的收益很小,也不适合作为第一轮测试对象。
打开浏览器开发者工具的 Network 面板,记录以下信息:
- 请求 URL、状态码和失败时间。
- 请求头中的
Accept-Encoding。 - 响应头中的
Content-Encoding、Content-Type、Content-Length、Vary、Age和 CDN 缓存状态头。 - 故障是否只发生在某个地区、某个节点、某类浏览器或登录状态下。
- 绕过 CDN、直接访问源站或临时切换到仅 DNS 解析后,结果是否变化。
不要只看页面最终显示。浏览器会自动解压响应,页面上的“乱码”可能来自下载内容本身,也可能是解压阶段已经失败。Network 面板和命令行保存下来的原始响应更容易区分这两种情况。
用 curl 分别请求 Brotli、Gzip 和未压缩版本
下面的测试会保存原始响应体,不让 curl 自动解压。把示例 URL 替换为实际故障资源:
url='https://example.com/assets/app.js'
curl -sS --raw -D br.headers -o br.body \
-H 'Accept-Encoding: br' "$url"
curl -sS --raw -D gzip.headers -o gzip.body \
-H 'Accept-Encoding: gzip' "$url"
curl -sS --raw -D identity.headers -o identity.body \
-H 'Accept-Encoding: identity' "$url"
查看三份响应头:
grep -iE '^(HTTP/|content-encoding:|content-type:|content-length:|vary:|age:|cache-control:|etag:)' \
br.headers gzip.headers identity.headers
正常情况下,三次请求可能得到三种结果:
- 请求
br时,响应头为Content-Encoding: br,响应体能够通过 Brotli 校验。 - 请求
gzip时,响应头为Content-Encoding: gzip,响应体能够通过 Gzip 校验。 - 请求
identity时,不应带Content-Encoding: br或gzip,响应体可以直接按声明的Content-Type读取。
可以继续检查压缩体是否完整:
brotli -t br.body
gzip -t gzip.body
如果服务器不对该资源压缩,缺少 Content-Encoding 并不一定是错误。小文件、已经压缩的格式、未被配置包含的 MIME 类型以及某些状态码,都可能被正常跳过。问题在于响应头声称使用了一种编码,而响应体却不是对应格式,或者缓存把某个编码版本发给了不接受它的客户端。
Content-Encoding 必须与实际响应体一致
Content-Encoding 描述服务器对表示内容做过的编码,客户端会根据这个字段解码。常见故障有以下几类:
压缩体存在,响应头却被删除
源站返回了 Gzip 或 Brotli 数据,中间代理、插件或错误的 Header 规则删掉了 Content-Encoding。浏览器会把压缩字节当作普通 CSS、JavaScript 或 HTML 处理,结果可能是乱码、语法错误或资源加载失败。
响应头声明压缩,正文却已经是明文
CDN 从源站取回压缩响应后完成了解压或内容改写,但仍保留原来的 Content-Encoding。浏览器再次尝试解压明文,常见表现就是 ERR_CONTENT_DECODING_FAILED。
同一份内容被重复压缩
源站、反向代理、缓存插件和 CDN 都可能具备压缩能力。如果一层没有正确识别上游已经编码的响应,又对其压缩一次,而响应头只记录了一层编码,客户端只能解开一层,剩余内容仍不可用。
HTTP 允许内容依次应用多层编码,但响应头需要完整列出编码顺序,客户端按相反顺序解码。普通网站没有必要故意叠加 Brotli 和 Gzip。生产配置应明确由哪一层负责压缩,并让其他层识别已有的 Content-Encoding。
检查 Vary: Accept-Encoding 是否覆盖所有缓存层
同一个 URL 可以根据 Accept-Encoding 返回 Brotli、Gzip 或未压缩内容。共享缓存需要知道这些响应不能混成一个对象。常见做法是在响应中加入:
Vary: Accept-Encoding
如果缺少这个字段,CDN、反向代理或其他共享缓存可能先存下 Brotli 版本,随后把它发给只接受 Gzip 或 identity 的客户端。也可能先缓存未压缩版本,导致支持 Brotli 的访客一直拿不到压缩内容。
需要注意,看到 Vary: Accept-Encoding 还不够。CDN 的缓存键、页面缓存插件和反向代理配置可能忽略或改写 Vary。应在源站直连和 CDN 域名上分别测试三种 Accept-Encoding,比较以下内容:
Content-Encoding是否随请求变化。ETag、Content-Length或响应体哈希是否对应不同变体。- CDN 的
HIT、MISS、Age是否符合预期。 - 第一次请求某个变体后,是否影响下一次请求其他变体。
Nginx 开启 gzip_vary on; 时会为可压缩响应加入 Vary: Accept-Encoding。Apache 的 mod_deflate 也会处理相应的 Vary 逻辑。不过,源站正确不代表 CDN 缓存键一定正确,最终仍要从外部重复请求验证。
把 CDN 和源站分开测试
只检查 CDN 域名,很难判断错误在哪一层。建议准备两条访问路径:
- 正常生产域名,通过 CDN 访问。
- 直接访问源站,或使用只解析到源站的测试域名,并保持正确的 Host 与 HTTPS 证书条件。
如果源站的三种响应都正常,而 CDN 路径出现编码错配,检查 CDN 的自动压缩、缓存键、边缘脚本、内容改写和缓存清理。如果源站直连已经错误,则先修复 Web 服务器、应用或缓存插件,避免 CDN 持续存入错误对象。
测试源站时不要绕过必要的 Host、SNI 和虚拟主机条件。直接访问 IP 可能命中默认站点,得到的结果与生产域名无关。可以使用 curl 的 --resolve 临时指定地址:
curl -sS --raw -D origin.headers -o origin.body \
--resolve example.com:443:203.0.113.10 \
-H 'Accept-Encoding: br, gzip' \
https://example.com/assets/app.js
将源站结果与 CDN 结果按状态码、响应头和原始响应体逐项比较。不要只比较文件大小,动态压缩级别不同也会造成体积变化。
检查 Nginx 和 Apache 的压缩范围
Nginx
Nginx 的 Gzip 配置通常涉及 gzip、gzip_types、gzip_min_length、gzip_proxied 和 gzip_vary。示例配置如下,具体 MIME 类型与阈值要按站点内容调整:
gzip on;
gzip_min_length 1024;
gzip_vary on;
gzip_types
text/plain
text/css
application/javascript
application/json
application/xml
image/svg+xml;
修改前保存原配置,随后执行:
sudo nginx -t
sudo systemctl reload nginx
如果站点使用预压缩文件,还要检查 gzip_static 或 Brotli 静态文件的生成与更新流程。源文件更新后,旧的 .gz、.br 文件没有同步更新,可能让浏览器持续拿到旧代码。
Apache
Apache 的 Gzip/Deflate 通常由 mod_deflate 提供,Brotli 由 mod_brotli 提供。检查模块是否启用、过滤器匹配的内容类型,以及反向代理或应用是否已经生成压缩响应。不要让不同层对同一响应重复应用输出过滤器。
无论使用哪种服务器,都不建议对 JPEG、PNG、WebP、AVIF、MP4、PDF、ZIP、Gzip 压缩包等格式盲目再次压缩。额外消耗 CPU 后,体积通常不会明显下降,配置错误时还会增加排障难度。
清理旧缓存后再判断修复是否生效
压缩配置改对后,CDN 仍可能保存旧的错误变体。只刷新浏览器无法清掉边缘节点缓存。按最小影响原则清理故障 URL,必要时再清理对应目录;全站清缓存会带来较大的回源流量。
清理后依次请求 identity、Gzip 和 Brotli,每一种至少测试两次:第一次观察回源或 MISS,第二次观察 HIT 与 Age。如果某个变体在第二次请求后覆盖了另一个变体,缓存键或 Vary 处理仍有问题。
还要检查 HTML、CSS、JavaScript 和 API 响应。CDN 可能只对部分 MIME 类型启用边缘压缩,不同缓存规则也可能分别生效。
用一张验证矩阵完成收尾
修复后可以按下面的矩阵记录结果:
| 访问路径 | 请求 Accept-Encoding | 期望 Content-Encoding | 响应体校验 | 第二次缓存状态 |
|---|---|---|---|---|
| 源站 | identity |
无 | 明文可读 | 不要求 |
| 源站 | gzip |
gzip 或按配置不压缩 |
gzip -t 通过 |
不要求 |
| 源站 | br |
br 或按配置不压缩 |
brotli -t 通过 |
不要求 |
| CDN | identity |
无 | 明文可读 | HIT/Age 合理 |
| CDN | gzip |
gzip 或受控转换结果 |
内容可解压 | HIT/Age 合理 |
| CDN | br |
br 或受控转换结果 |
内容可解压 | HIT/Age 合理 |
“按配置不压缩”是允许的,前提是响应头和响应体一致。CDN 也可能根据产品策略对源站内容重新压缩或规范化,判断标准仍然是客户端声明的能力、最终 Content-Encoding 与实际响应体三者对应。
最后记录压缩责任层、启用的 MIME 类型、最小压缩大小、CDN 缓存键、缓存清理方法和验证 URL。以后更换 CDN、缓存插件、Web 服务器或构建流程时,可以直接按这份记录回归三种编码变体。
参考资料
- MDN:Content-Encoding 响应头
- MDN:Vary 响应头
- RFC 9110:HTTP Semantics
- Nginx 官方文档:ngx_http_gzip_module
- Apache HTTP Server 2.4:mod_deflate
- Apache HTTP Server 2.4:mod_brotli
- Cloudflare 文档:Compression
资料核验日期:2026-09-23。




