Appearance
内容协商
本章导读:同一个资源,浏览器想要 HTML、移动端想要 JSON、报表系统想要 CSV——内容协商(Content Negotiation)就是让"一个 URI,多种表示"合法共存的协议机制(RFC 9110 §12)。本章讲清请求驱动与响应驱动两条路线、q 值权重规则、
Vary头与缓存的致命配合,以及工程上"要不要做协商"的取舍。
1. 协商的两个方向
1.1 请求驱动协商(Proactive / Server-driven)
客户端通过头部声明偏好,服务器选择表示:
http
GET /articles/42 HTTP/1.1
Accept: application/json;q=0.9, text/html;q=0.8, */*;q=0.1
Accept-Language: zh-CN,zh;q=0.9,en;q=0.7| 请求头 | 协商维度 |
|---|---|
Accept | 媒体类型 |
Accept-Language | 自然语言 |
Accept-Encoding | 编码(gzip/br——严格说不是"表示"协商而是传输优化) |
Accept-Charset | 字符集(已废弃使用,UTF-8 时代无需) |
1.2 响应驱动协商(Reactive / Agent-driven)
服务器返回候选表示的链接列表(3xx Multiple Choices),由客户端自己挑——几乎不用,仅存在于规范里。工程上等价物是显式查询参数:/articles/42?format=csv(见 §4)。
2. Accept 的匹配规则(RFC 9110 §12.5.1)
媒体类型范围(range)语法:type/subtype、type/*、*/*,带 q 权重(0~1,缺省 1,精确匹配优先于通配):
客户端: Accept: text/*;q=0.5, application/json;q=0.9
候选: text/html (q=0.5) application/json (q=0.9) text/csv (q=0.5)
结果: 按 (媒体范围特异性, q) 排序 → application/json 胜出规则要点:
- 特异性优先:
application/json匹配优于*/*匹配(先比 type 再比 q); - 结构后缀也可通配:
Accept: application/*+json能匹配application/problem+json、application/merge-patch+json——自定义媒体类型请带+json后缀的理由在此; - 无任何可接受表示 →
406 Not Acceptable(响应体可为*/*兼容格式); - 客户端没发
Accept≡Accept: */*。
3. Vary:协商系统的缓存命门
内容协商 + 缓存 = 事故高发区。缓存键默认只有 URI——若 Accept: json 与 Accept: xml 的响应共用一个缓存槽位,就会串表示。
http
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept, Accept-Language ← 声明:这两个请求头参与缓存键铁律:凡是响应内容随请求头而变(Accept/Accept-Language/认证角色/版本头部),就必须声明对应的 Vary。
Vary: *= 永不可缓存(谨慎);- 过多的 Vary 维度会让缓存命中率崩塌(CDN 对 Vary 支持参差)——这是"少协商、多显式"的运维理由。
4. 工程现实:三种"协商"策略对比
| 策略 | 例 | 优点 | 缺点 |
|---|---|---|---|
纯 Accept 头协商 | Accept: application/vnd.example.v2+json | 最符合 REST 正统;URI 稳定 | 不可浏览、CDN/代理对 Vary 支持弱、调试麻烦 |
| 扩展名/查询参数 | /articles/42.csv、?format=csv | 直观、可缓存、可分享 | URI 携带了"表示"信息,被认为不纯;格式爆炸会污染路由 |
| 默认单一表示 | 一律 JSON,特殊需求走专门子资源 | 简单,99% 团队场景够用 | 少数消费方(Excel 报表)需自行转换 |
主流实践建议:
- API 默认只输出 JSON,不实现
Accept多类型分支(YAGNI); - 导出类需求设计为动作端点 + 作业资源:
POST /articles/exports {format:"csv"}→GET /jobs/9→Location: /files/xxx.csv; - 版本演进若走头部协商(如 GitHub 的
Accept: application/vnd.github+json加日期版本头),务必在文档给出等价默认。
TIP
"服务器能不能给我别的表示"与"我要不要别的表示"是两回事。99% 的所谓"内容协商需求"其实是媒体上传/导出转换需求——用作业资源解决,别把协商机制复杂化。
5. 语言协商与本地化
GET /articles/42
Accept-Language: zh-CN,zh;q=0.9,en;q=0.7- 资源数据的多语言(title 中/英)与错误消息的多语言(
problem+json的title/detail)都可用 Accept-Language; - 若翻译版是独立资源(i18n 场景常见),用 URI 区分:
/zh/articles/42或/articles/42?lang=zh—— 此时它们是不同资源的不同 URI,不是同一资源的协商; - 决定:同一资源多表示(可随 UA 变) vs 多资源(URL 可分享且稳定)——面向用户的文案资源建议后者。
6. 本章小结
- 请求驱动协商四件套:
Accept/Accept-Language/q权重 /406;特异性匹配优先于权重。 Vary是协商的缓存正确性开关,漏写 = 串数据事故。- 工程上默认单 JSON 表示 + 导出走作业,是绝大多数团队的最优解;头部协商留给确有需要的平台型 API。
7. 下一步
缓存与条件请求是 Vary/ETag 的主场 → 缓存与条件请求