内容协商 (Content Negotiation) 是 HTTP 通信中客户端 (浏览器) 与服务器协商所请求资源最优表现形式的机制。同一 URL 可以根据用户的语言设置、设备类型和支持的格式返回不同的内容。
内容协商主要通过 HTTP 请求头实现。Accept 头部 (支持的媒体类型:text/html、application/json 等)、Accept-Language 头部 (期望的语言:ja、en-US 等)、Accept-Encoding 头部 (支持的压缩方式:gzip、br 等) 是最常见的协商头部。这些头部都可以附带 q 值来表示优先级,例如 Accept-Encoding: br, gzip;q=0.8 表示首选 br,不支持时接受 gzip。服务器若根据这些头部改变响应内容,就需要在 Vary 头部中列出用于判断的头部名称。漏掉 Vary: Accept-Language 时,缓存或 CDN 会把最先取得的那个语言版本发给后续所有访问者。
多语言网站中的内容协商与短链接密切相关。同一短链接被点击后,可以根据用户浏览器的语言设置重定向到中文页面或英文页面。但从 SEO 角度来看,更推荐为每种语言设置独立的 URL 并通过 hreflang 标签关联。基于内容协商的语言切换可能导致搜索引擎无法正确索引各语言版本。Google 同样建议为每个语言版本准备独立 URL,而不是依据 Cookie 或浏览器设置切换显示,并要求避免把访问者从一个语言版本自动重定向到另一个语言版本,因为这类跳转会让用户与搜索引擎都无法看到全部版本。Google 的抓取程序在请求中不会附带 Accept-Language 头部,因此仅凭该头部切换显示时,被抓取到的只有默认语言版本。根据 IP 推断地区来改变内容的做法同样不建议采用。
API 设计中内容协商同样重要。短链接服务的 API 根据 Accept 头部返回 JSON 或 XML 响应是常见做法。为同一资源准备多种表现形式,可以让调用方选择自己能处理的格式。不过具体选择哪一种表现的算法并未由 HTTP 规范规定,而是交由服务器实现决定。遇到不支持的格式时是返回 406 (Not Acceptable),还是忽略该头部返回默认格式,同样属于实现层面的判断。
内容协商分为服务器驱动型 (服务器根据请求头判断) 和代理驱动型 (服务器提供选项由客户端选择) 两种。HTTP 规范中将前者称为 proactive negotiation,后者称为 reactive negotiation。实际使用中以前者为主,但其代价是根据请求头做出的推断始终带有一定的随意性。后者未能普及,原因在于列出选项的页面格式没有规范定义,导致选择过程无法自动化,而且在取得真正的资源之前会多出一次往返。