bigywater.tech
Flask · 44

ETag 与条件请求

浏览器带来的 If-None-Match 怎么处理?ETag 怎么生成才合理?强弱 ETag 有什么区别?能不能用 ETag 防止两个人同时修改?这篇用 test client 逐个验证。

304
If-None-Match 命中时的状态码(正文 0 字节)
412
If-Match 不匹配时的状态码
58890 → 0
1 万个整数的 JSON:200 与 304 的正文字节数

add_etag 与 304

实测 · 本机执行输出
  首次: 200 | ETag = "a9c15cbfcf470ec22cfefbd5f3e50aec62b048b8" | 正文 401 字节 | Content-Length 401
  与 sha1(正文) 比较: True
  If-None-Match = "a9c15cbfcf4… → 304 | 正文 0 字节 | ETag = "a9c15cbfcf4… | Content-Length = None
  If-None-Match: "other" → 200 | 正文 401 字节
  If-None-Match: * → 304
  If-None-Match 带多个值(含正确的)→ 304
  POST 带 If-None-Match → 405 (路由不允许 POST)

上一篇:send_file() 与 send_from_directory();下一篇:Last-Modified 与缓存响应。

弱 ETag

实测 · 本机执行输出
  响应的 ETag: W/"abc"
  If-None-Match: 强 "abc"      → 304
  If-None-Match: 弱 W/"abc"    → 304
  If-None-Match: 不同 "xyz"     → 200
  werkzeug 的辅助:generate_etag(b"x") = 11f6ad8ec52a2984abaafd7c3b516503785c2072 | quote_etag("a", weak=True) = W/"a" | parse_etags('W/"a", "b"').to_header() = "b", W/"a"

手写 ETag

实测 · 本机执行输出
  GET /manual → 200 "2526bca18f967ecc34148598c3828416"
  带 ETag 再请求 → 304 (没有重新生成响应体)

If-Match 与丢失更新

实测 · 本机执行输出
  第一个客户端 PUT If-Match = 它读到的 ETag → 200 updated
  第二个客户端用同一个(已过期的)ETag PUT → 412 precondition failed
  数据版本现在是: {'v': 2}

304 省了什么

实测 · 本机执行输出
  200:正文 58890 字节 + 头约 150 字节;304:正文 0 字节 + 头约 87 字节;但服务器仍然生成并哈希了整个响应体(耗时见下)
  test client 耗时:200 465 µs;304 475 µs(服务器端的计算没有省:还是要生成响应体算 SHA-1)

分步演示

ETag
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动
    • 没有验证:CDN 与浏览器对 ETag 的真实处理、gzip 压缩后 ETag 是否应变成弱 ETag、分布式部署下多实例 ETag 的一致性、If-Range 与 ETag 的组合(见第 49 篇)
    京ICP备2026067502号-1京公网安备11010502063487号