缓存阶段讲完了,现在进内容与编码阶段。先讲一个基础问题:同一个 URL,服务器怎么知道要返回什么格式、什么语言、什么压缩方式?

比如 /api/user 这个 URL,浏览器请求可能想要 JSON,老系统可能想要 XML,手机端可能想要压缩版,外国用户想要英文版。URL 一样,但客户端期望的「表示(representation)」不同。这个「一个资源,多种表示」的协调过程,就是内容协商

两种协商方式

HTTP 内容协商分两种:

主动协商 vs 响应式协商

主动协商(Proactive,服务端驱动)

客户端在请求头里表达自己想要的,服务器根据这些头决定返回什么。这是绝大多数实际场景用的方式。相关的请求头:

  • Accept: application/json —— 我想要 JSON
  • Accept-Language: zh-CN —— 我想要中文
  • Accept-Encoding: gzip, br —— 我能接受 gzip 或 br 压缩
  • Accept-Charset: utf-8 —— 我接受 utf-8 字符集(现代基本都 utf-8,这个头用得少了)

服务器读这些头,决定返回哪种表示,并在响应里用 Content-TypeContent-LanguageContent-Encoding 告诉客户端「我给了你哪种」。

响应式协商(Reactive,代理/客户端驱动)

服务器不知道客户端想要什么,但它把所有可选表示列出来,让客户端自己选。响应状态码是 300 Multiple Choices,body 里列出每个表示的 URL。客户端选一个再请求。

这种方式几乎没人用,因为它要多一次往返(先拿到选项列表,再请求选中的)。现代应用都用主动协商。

主动协商的关键:q 值表达优先级

主动协商里,客户端用 q 值(quality factor)表达偏好优先级。q 是 0 到 1 的权重,默认 1:

q 值表达内容协商的优先级

Accept-Language: zh-CN, zh;q=0.9, en;q=0.8

这个头的意思是:最想要 zh-CN(q=1 默认),其次 zh(q=0.9),再次 en(q=0.8)。服务器据此选它能提供的、客户端最想要的那个。如果服务器只有英文,就返回英文(q=0.8 总比没有强);如果都有,返回 zh-CN。

q=0 表示「不要这个」。Accept-Encoding: gzip, br;q=0 意思是「我要 gzip,但别给我 br」。

Accept-EncodingAccept-CharsetAccept-Language 都用这套 q 值语法。理解 q 是权重不是质量评分,是读懂这些头的关键。

服务器不一定满足 Accept

有个反直觉的点要强调:Accept 是客户端的期望,服务器不一定满足

客户端说 Accept: application/json,服务器完全可以返回 HTML(如果它只支持 HTML),只要状态码正常。客户端要自己检查响应的 Content-Type,别假设服务器一定按 Accept 返回。

如果服务器实在提供不了客户端能接受的任何格式,应该返回 406 Not Acceptable——「你想要的格式我都没有」。这个状态码告诉客户端「别等了,换种格式再试」。

Vary:协商和缓存的桥梁

上一篇讲过 Vary,这里从内容协商角度再串一遍。同一个 URL,因为内容协商,服务器可能返回不同表示。缓存必须区分这些表示,否则会串——把 JSON 版给想要 XML 的客户端。

这就是为什么协商维度必须出现在 Vary 里。如果服务器根据 Accept-Encoding 返回不同压缩版,响应头必须带 Vary: Accept-Encoding,告诉缓存「这个响应依赖 Accept-Encoding,缓存时要把它算进键」。

所以内容协商和 Vary 是一对:协商维度决定了「一个 URL 有几种表示」,Vary 告诉缓存「按什么维度区分这些表示」。漏配 Vary 就串内容,这是内容协商最常见的 bug。

取舍与边界

除了协商,还有一种把格式写进 URL 的简化思路,对比一下就知道为什么现代 API 多放弃协商:

内容协商 vs URL 后缀:两种「一个资源多表示」的思路

内容协商有几个实践要点:

  • Accept 头太详细会降低缓存命中率。如果客户端发 Accept: application/json, application/xml, text/plain, */*,服务器可能针对不同组合返回不同内容,缓存键就爆炸了。实际中很多 API 不做严格协商,固定返回 JSON,省掉这个复杂度。
  • URL 后缀也是一种协商/api/user.json/api/user.xml 用文件后缀区分格式,本质是把协商维度放进了 URL。这种方式缓存友好(每个 URL 一个表示,不用 Vary),但牺牲了「一个资源多个表示」的纯粹性。
  • 现代 API 倾向「一个 URL 一种格式」。REST API 通常固定返回 JSON,不做内容协商——简单、缓存友好、客户端不用猜。

收束:内容协商是「一个资源,多种表示」的协调

内容协商解决「同一个 URL 给不同客户端不同内容」。主动协商靠 Accept 头表达期望,服务器据此选择;Vary 把协商维度告诉缓存,避免串内容。理解这套机制,你才能解释「为什么同个 API 在不同环境返回不同格式」。

下一篇讲压缩传输——内容协商确定了「返回什么」,但传输时怎么让它更小?这就是 Content-Encoding 和压缩算法的事。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

十三Tech公众号二维码