分析阅读约 2 分钟

ETag 监控:一个 no-cache 请求头改变了结果

一次真实的代理测试发现,仅仅一个请求头就把 304 响应变成了完整下载。本文查看实测的字节数、请求策略和可复现的数据记录。

本页内容

在为内容监控购买更多代理流量之前,先检查你的客户端实际发送的请求头。在我们的线上网站上,添加 Cache-Control: no-cache 后,对未变更页面的检查从不带正文的 304 响应变成了完整的 200 下载,尽管请求中携带了匹配的 ETag。

在一次受控比较中,12 个不带该覆盖项的条件请求传输了零字节响应正文;12 个带该覆盖项的请求传输了 271,968 字节未变更的 HTML。 为获取 ETag 所需的 6 次初始下载另外耗费了 135,984 字节,并已计入公开的记录。

这是关于一个已部署的 Next.js 16.3.4 站点和一种特定请求策略的发现。它不是建议你从监控中去掉新鲜度要求。真正有用的决定是:是否更改客户端策略、选择更小的表示形式,或者调查服务器的重新验证行为。

任务:监控已知内容,同时避免不必要的下载

我们测试了两篇已有的 ipvolt 文章:代理基准测试指南代理错误说明,各自以 HTML 和 Markdown 两种形式。我们的监控器关注文章标题和两个指定的章节标题。它还检查媒体类型,对于 HTML,还检查页面标题和 canonical URL。

这些检查能确认被关注的信息存在。它们并不能让 Markdown 替代页面布局、JavaScript 行为或可正常工作的注册流程。确切的断言见研究配置

采集使用了面向美国、英国和荷兰的第三方认证代理连接。初始实验还包含一个直连对照组。国家检查响应在各检查点都报告了所请求的国家;在初始的美国运行期间出口地址发生过变化,因此这些是路线观察结果,而不是固定 IP 或全国范围的可靠性结果。连接提供商未予披露;这不是对可用的 ipvolt 代理服务的测试。

三项 HTTP 实验都在 2026 年 9 月 11 日运行。它们共发出 234 个请求,传输了 2,975,066 字节响应正文,包括初始下载、验证下载和未成立的假设。每一个尝试的请求都在没有 curl 传输错误的情况下完成。额外的地理探测和浏览器 QA 单独记录在方法说明中;它们不在该 HTTP 实验总数之内。

更小的首次下载并没有回答缓存问题

初始实验一致地请求 gzip,并测量收到的正文字节数。每种表示形式各 12 次初始观察,大小如下:

文章表示形式每次初始请求收到的正文字节数
基准测试 HTML26,521
基准测试 Markdown7,056–7,061
错误指南 HTML18,807
错误指南 Markdown5,081

两个 HTML 端点都提供了 ETag。在本实验中,两个 Markdown 端点都没有提供 ETag 或 Last-Modified 验证器。不带 Cache-Control 覆盖项的 HTML 条件检查返回了 304;运行程序复用了此前已接受的匹配正文,而不是把空响应解释为一份新文档。

探索性运行发出了 144 个请求:120 个完整的 200 响应和 24 个被接受的 304 响应。它跳过了 36 个条件请求槽位,因为 Markdown 没有验证器,或者首页被标记为 no-store。被跳过的请求既不计为成功,也不计为零字节网络观察。

随后我们通过三条代理路线中的每一条,对每种表示形式连续运行了 5 次检查。每次检查都显式发送 Cache-Control: no-cache,客户端在检查之间保留了符合条件的验证器。全部 60 个请求都返回了 200,包括 24 次携带匹配 ETag 的 HTML 复检。「重复的 HTML 验证会避免下载正文」这一预期在该策略下并不成立。

隔离出改变响应的那个请求头

为了调查,我们保持 URL、路线配置、Accept、语言、gzip 偏好和确切的种子 ETag 不变。对于六种路线/文章组合中的每一种,我们发出一次种子请求和四次条件请求。顺序为 A/B/B/A 或 B/A/A/B,其中 A 不带 Cache-Control 覆盖项,B 发送 no-cache

条件请求策略尝试次数响应收到的正文字节数
匹配的 If-None-Match;无 Cache-Control 覆盖项1212 × 3040
相同的 If-None-Match;Cache-Control: no-cache1212 × 200271,968

全部 24 次条件观察都与种子的关注值和解码后内容一致。算上 6 次种子请求,这次受控实验在 30 个请求中传输了 407,952 字节正文。它使用的是平衡排序而非随机抽样,确定的是这些端点在这段短暂窗口内观察到的行为。

实现代码解释了一个合理的机制。我们把实际部署的依赖文件与本地的 Next.js 16.3.4 安装进行了核对:相关源码哈希一致。它的 ETag 响应辅助函数在返回 304 之前会调用 fresh;捆绑的新鲜度检查在评估匹配的 ETag 之前,一旦请求包含 no-cache 就返回 false。一次单独的本地函数测试复现了同一分支。Next.js 响应辅助函数fresh 实现

这是与线上结果相符的、特定于技术栈的行为。HTTP 并没有把 no-cache 定义为一条要求下载完整正文的通用指令。收到 304 也不能证明请求到达了源站而不是某个作出响应的缓存。HTTP 缓存

有用的优化取决于你需要的新鲜度

对于显式使用 no-cache 的五次检查实验,实际总计如下:

表示形式请求数(含初始下载)收到的正文字节数
HTML30679,920
Markdown30182,070
差值请求数相同497,850

在该策略下,对于被关注的内容,Markdown 传输的响应正文字节数少了 73.2%。 它没有减少请求数。这些是实测的载荷差异,不是实测的账单节省,也不是对月度流量的预测。curl 的下载大小指标不包含请求头和其他网络开销。curl 载荷统计

当新鲜度要求允许时还有另一个选项:Markdown 响应声明了 public, max-age=300, must-revalidate。缓存可以在不发出请求的情况下复用仍然新鲜的已存储表示。缺少验证器并不会使其不可缓存。我们的运行程序有意通过网络进行检查;它们没有实现这种基于新鲜度的调度策略。新鲜度与复用

用这个结果来选择下一步动作:

你的监控需求本案例支持的动作
可以接受在服务器的新鲜度窗口内复用在安排另一次下载之前,评估一个感知新鲜度的本地缓存。
要求响应服务器或缓存对每次检查都进行验证用实际端点测试最终的请求头;不要假设 ETag 保证得到不带正文的响应。
需要被关注的文本,而每次检查都返回完整正文把压缩后的 HTML 与包含同样事实的文本或 Markdown 表示进行比较。
需要渲染后的布局或交互流程保留浏览器检查;更小的文本表示覆盖不了这项工作。

不要仅仅为了复现零字节那一行而去掉 no-cache。两种请求头策略可能带来不同的新鲜度后果。先确定你的监控器必须检测什么,然后再对允许的实现进行测量。

复现结果并调整检查

离线分析脚本脱敏后的记录下载到同一目录。Python 3.10 或更高版本即可;此命令不发出任何网络请求:

sh
python3 reproduce.py recordings.zip

它会重新计算尝试次数、正文总量、请求头比较和 Markdown 百分比。把它的输出与记录的结果进行比较。数据集包含时间戳、请求策略、选定的响应头、内容检查和哈希值。凭据、出口 IP、未过滤的请求头和原始正文捕获仍然保密;回放验证的是公开的算术结果,而不是独立地对历史响应进行鉴真。

下载说明链接了确切的采集程序、配置和 27 个通过的离线测试。线上程序需要 curl 8.4 或更高版本,保留 TLS 验证,对请求和正文传输设限,并通过 curl 的输入私密地传递代理认证。它们的允许列表仅限于本案例的公开站点;对于你自己运营的资产,请同时调整允许列表和内容断言。

我们没有观察到真实的内容更新,也没有测量检测延迟。所有测试端点的解码后内容都保持稳定;有少数 gzip 字节流在解码文本完全相同的情况下有所不同。这是一个应当比较监控器真正关心的内容的理由,而不是本研究消除了运营误报的证据。这只是一个站点、两篇文章和一段短暂的观察窗口——不是提供商排名,也不是通用的缓存基准。

方法:智能体辅助的采集与分析,并有独立的证据、编辑和搜索审阅。所有报告的网络观察都是实际执行的;离线测试使用受控的测试数据,并单独标注。

访问开放时通知我。开放时只发一封邮件,没有别的。

参考来源

  1. RFC 9110: If-None-Match and conditional requests
  2. RFC 9111: request and response cache directives
  3. curl: downloaded response-body size
  4. Next.js 16.3.4: ETag response handling
  5. fresh 0.5.2: request freshness evaluation

标签:ProxiesTroubleshooting