qinghuan
← 回到知识探索

后端现场 / 趣味知识

304 没有正文,网页为什么还能显示?

客户端拿旧版本来核对,服务器确认可复用后就不必再传一遍。

展开本文目录
  1. ETag 是核对版本的标签
  2. 少传内容,不等于什么都没发生
  3. 何时核对,由缓存规则另行决定
  4. 看到 304,仍要确认核对的是哪份内容
  5. 记住这三点
  6. 别把例子用过头
  7. 自己试一小步
  8. 查原始资料
  9. 找一份资料继续练
开始前,会这些就够了
  • 知道 HTTP 请求和响应
遇到不熟悉的词,先看这 4 个解释
HTTP 请求与响应
客户端用请求说明想访问什么、做什么;服务端用响应说明处理结果,并按需要带回内容。一次网页操作可能触发多组这样的消息交换。
HTTP 状态码
响应里的三位数字,用来说明这次请求处理到了哪种结果,例如成功、需要重定向或发生错误;程序会结合响应内容决定下一步怎样处理。
接口
程序对外提供功能时,约定怎样调用、传哪些信息、返回什么结果;调用方按这份约定使用功能,不必先了解服务内部每一行实现代码。
缓存
把已有数据或计算结果保留一份,后续满足条件时直接复用,减少重复读取或计算;这份副本何时更新、何时失效,需要另外制定规则。
打开完整术语小抄 ↗

假设你第二次打开一份课程说明,网络面板显示 304,响应里找不到完整正文,页面却正常出现。这不是浏览器凭空补全,也不是服务器漏发了内容:这次交互可能只是在核对浏览器已经保存的那份内容。先把“是否能继续用”和“重新下载”分开,就容易理解。

ETag 是核对版本的标签

服务器第一次返回内容时,可以附带 ETag。客户端保存内容和标签,下次发送 If-None-Match,意思是让服务器在当前表示与这些标签不匹配时再发送内容。对于符合条件的 GET 或 HEAD,标签匹配时可返回 304,客户端继续使用对应的已存内容。

ETag 是服务器生成的不透明标签,客户端不应凭外形猜它一定是文件 MD5,也不应自己随便改写引号。服务器可以采用适合自己的版本生成方式,但需要让标签与所验证的表示可靠对应。相同 URL 的不同语言、压缩方式等表示,也可能需要不同处理。

少传内容,不等于什么都没发生

下面只模拟一种资源、一个强标签和 GET 的判断分支。第一次得到正文后,客户端保留它;第二次收到 304,使用已经保存的正文。这段程序没有联网,也没有实现 HTTP 头解析。它特意让“缓存的正文”与“本次响应的正文”成为两个变量,避免混在一起。

真实 If-None-Match 支持标签列表、星号以及弱标签比较,不能直接把这里的字符串相等判断搬成完整服务器实现。304 本身不携带正文,还会按规范返回需要更新缓存的信息。收到 304 并不意味着无需处理响应头,也不意味着没有网络往返。

小实验 · python
def get_document(client_tag):
    current_tag = '"course-v2"'
    if client_tag == current_tag:
        return 304, current_tag, None
    return 200, current_tag, "course revision 2"

status, tag, saved_body = get_document(None)
print(status, saved_body)
status, tag, response_body = get_document(tag)
shown_body = saved_body if status == 304 else response_body
print(status, response_body, shown_body)

怎样运行:Python 3.10+;保存为 etag_demo.py,运行 python etag_demo.py。

运行后对照结果
200 course revision 2
304 None course revision 2

何时核对,由缓存规则另行决定

ETag 解决的是核对版本,不直接回答客户端可以多久不询问服务器。新鲜度由 Cache-Control 等规则参与决定:新鲜的缓存可能直接复用,不产生请求;需要验证时才带条件询问。响应中的 no-cache 通常允许存储,但要求复用前成功验证,因此名字容易让人误解。

如果客户端丢了正文却还保留标签,再收到 304,就没有可展示的完整数据。应用自行管理缓存时,应该让内容和验证标签一起管理,并为缺失情况重新获取内容。不能把标签当作正文,也不能用上一位用户保存的内容来填补当前用户的页面。

看到 304,仍要确认核对的是哪份内容

假设应用从旧业务缓存生成正文和标签,即使这次条件请求到达服务器,也可能继续核对那份旧数据。304 证明的是按该响应路径完成了条件判断,不是整套系统里每份数据库与缓存都已最新。排查旧页面时,要把 HTTP 验证和业务数据更新分开。

可以在网络面板对照第一次 200 的 ETag、第二次请求的 If-None-Match、第二次状态码与缓存信息。阿里云 OSS 的 GetObject 文档也提供了条件下载的具体接口例子;阅读产品文档时,应继续核对它的权限、编码和条件组合规则,不能只复制一个状态码。

记住这三点

  • 304 让客户端复用对应的已存内容,不会携带新的正文。
  • ETag 用于核对表示,不应被客户端当作固定哈希格式解析。
  • 缓存何时需要验证,与验证标签是否匹配是两步判断。

别把例子用过头

  • 代码仅演示单个已知强标签,不是完整的 If-None-Match 解析器。
  • 304 仍可能涉及网络与服务器工作,并非零成本读取。
  • HTTP 验证不能保证应用底层数据一定最新。

自己试一小步

如果服务器内容变了,标签却没有变,客户端可能观察到什么?

  1. 在模型中为两个版本分别设计内容和标签。
  2. 先只改内容,再同时改内容和标签,比较流程。
  3. 写出缓存正文丢失、标签仍在时的恢复步骤。

做到这些,就算完成

  • 能区分本次响应正文与已经缓存的正文。
  • 没有把模型字符串比较当成全部协议规则。
  • 说明恢复时为什么不能直接展示空正文。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • RFC 9110:HTTP Semantics (新标签页)↗IETF · 英文

    第 8.8.3、13.1.2、15.4.5 节支持 ETag、If-None-Match 弱比较、GET/HEAD 的 304 与无正文要求。

    公开英文标准;ETag、条件请求和状态码需结合相应方法与条件理解。

  • GetObject:获取对象 (新标签页)↗阿里云 OSS · 中文

    直接核对请求头表中的 If-None-Match 与 200/304 行为,以及权限和压缩处理的产品说明。

    公开中文产品文档;条件请求、权限和返回信息以阿里云 OSS 接口约定为准。

  • RFC 9111:HTTP Caching (新标签页)↗IETF · 英文

    第 4.3、5.2 节支持缓存验证、新鲜度和 no-cache 的区别。

    公开英文标准;讨论 HTTP 缓存,不覆盖应用内部所有缓存规则。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。