Skip to content

内容协商

本章导读:同一个资源,浏览器想要 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/subtypetype/**/*,带 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 胜出

规则要点:

  1. 特异性优先application/json 匹配优于 */* 匹配(先比 type 再比 q);
  2. 结构后缀也可通配:Accept: application/*+json 能匹配 application/problem+jsonapplication/merge-patch+json——自定义媒体类型请带 +json 后缀的理由在此;
  3. 无任何可接受表示 → 406 Not Acceptable(响应体可为 */* 兼容格式);
  4. 客户端没发 AcceptAccept: */*

3. Vary:协商系统的缓存命门

内容协商 + 缓存 = 事故高发区。缓存键默认只有 URI——若 Accept: jsonAccept: 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/9Location: /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+jsontitle/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 的主场 → 缓存与条件请求