ETag是HTTP响应头中的资源实体标签,用于缓存协商;Flask默认不自动生成或校验ETag,需手动基于响应体、数据源版本或请求参数生成并比对If-None-Match头,否则缓存可能返回过期数据。

ETag 是什么,为什么 Flask 里值得用
ETag 不是装饰器,也不是 Flask 自带的中间件,它是一个 HTTP 响应头,由服务端生成、客户端缓存并下次带上 If-None-Match 请求头。Flask 默认不自动计算或校验 ETag,但能轻松接入——关键在于你控制响应体生成逻辑时,同步生成可复现的标识。
适合用 ETag 的场景:API 返回 JSON 数据(如 /api/users),内容变动不频繁但客户端频繁轮询;静态资源代理层缺失时,靠应用层做轻量缓存协商;避免序列化开销(比如不用重跑 json.dumps() 就能返回 304)。
手动设置 ETag 的三种常见方式
Flask 不强制你用某一种方法,选哪种取决于你的数据来源和性能敏感点:
-
基于响应体哈希:最稳妥,适用于纯函数式视图(无副作用),但每次都要计算
hashlib.md5(response_data.encode()).hexdigest(),对大响应体有开销 -
基于数据源版本:比如数据库记录的
updated_at或version字段,用str(record.updated_at.timestamp())当 ETag 值,快且语义清晰,但需确保字段更新及时 -
基于请求参数组合:如分页接口
/items?page=2&sort=name,把参数字典排序后哈希,适合缓存“查询结果”而非“原始数据”
注意:ETag 值必须用双引号包裹,例如 "abc123",否则部分客户端(如 curl 旧版、某些浏览器)可能忽略;Flask 的 make_response() 不会自动加引号,得自己拼。
立即学习“Python免费学习笔记(深入)”;
如何让 Flask 自动处理 ETag 校验
Flask 没有内置 etag 装饰器,但你可以用 after_request + 手动比对,或者更推荐:在视图内直接判断 request.headers.get("If-None-Match")。
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
典型写法:
@app.route("/api/data")
def get_data():
data = {"items": [1, 2, 3]}
json_body = json.dumps(data)
etag = f'"{hashlib.md5(json_body.encode()).hexdigest()}"'
if request.headers.get("If-None-Match") == etag:
return "", 304
resp = make_response(json_body)
resp.headers["ETag"] = etag
resp.headers["Content-Type"] = "application/json"
return resp
别漏掉这三点:304 响应不能带 Content-Type 或响应体;ETag 和 If-None-Match 必须完全相等(包括引号);如果用了 gzip 压缩,ETag 应基于压缩前内容计算,否则协商会失效。
用 Flask-Caching 配合 ETag 的坑
很多人想用 Flask-Caching 的 @cache.cached() 自动生成 ETag,但它默认只缓存响应体,不暴露哈希值供协商——也就是说,缓存命中时直接返回 200,跳过了 ETag 检查流程。
要让它配合 ETag,得手动接管:
- 禁用自动响应包装(设
cache.memoize而非@cache.cached) - 缓存键用
make_cache_key显式构造,同时保存对应 ETag 值到 Redis 或内存 - 视图中先查缓存键 → 取出数据 + 对应 ETag → 再做
If-None-Match判断
简单项目不如直接手写 ETag;复杂系统里,ETag 和缓存应分层:底层用 Redis 缓存数据,上层用 ETag 协商响应,别指望一个库包揽所有。
ETag 的真正难点不在生成,而在于“什么时候该变”。时间戳容易,但多数据源 join 后的结果,ETag 该以哪个表的更新时间为准?这个逻辑一旦写错,客户端就会长期看到过期数据,而且毫无报错提示。

















