内容协商(Content Negotiation)是 HTTP 协议中一个容易被忽视却相当实用的机制。当同一个 URL 对应多个不同格式的资源时,比如 index.html、index.php 和 index.html.gz 同时存在,服务器需要一种规则来决定到底返回哪一个。Apache 的做法是解析客户端请求头中的质量因子(即 q 值),对每个候选变体打分排序,最终选出综合得分最高的那个。理解这套算法,对调试缓存问题、配置多语言站点都很有帮助。

质量因子到底是什么
质量因子是 HTTP 协议中表达偏好程度的数值,取值范围从 0 到 1,保留三位小数。它出现在 Accept、Accept-Language、Accept-Encoding、Accept-Charset 等请求头中,紧跟在对应的媒体类型或语言标签后面,用分号分隔。数值越接近 1 表示客户端越希望收到这种资源,数值为 0 则表示明确拒绝。例如下面的请求头:
Accept: text/html,application/xhtml+xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.9,en;q=0.7 Accept-Encoding: gzip, deflate, br;q=1.0
这段头部信息的含义是:客户端最想要 text/html 格式的资源,q 值省略时默认等于 1;application/xhtml+xml 的权重是 0.9;最后的 */* 表示任何其他类型都能接受,但权重只有 0.8。语言方面,简体中文优先级最高,英文次之。需要注意的是,q 值表达的是相对偏好,而不是绝对评分,服务器是在所有候选资源之间做横向比较。
一个常见的误解是认为 q 值可以超过 1 或者写成百分比。RFC 规范明确限定其范围为 0 到 1 之间的三位小数,写成 q=1.5 或 q=90% 都属于非法格式,Apache 解析这类头时会直接忽略该条目或回退到默认值。
Apache 如何计算变体的综合得分
Apache 中负责内容协商的核心模块是 mod_negotiation,它支持两种工作方式:一是基于 type-map 文件的显式协商,二是基于目录扫描的隐式协商(即 MultiViews)。无论哪种方式,打分逻辑是一致的:服务器会针对每个变体,在媒体类型、语言、字符集和编码四个维度上分别计算质量值,然后相乘得到综合质量分。
以一个 type-map 文件为例,假设站点为首页提供了中文和英文两个版本:
# /var/www/html/index.html.var URI: index.zh.html Content-type: text/html;charset=UTF-8 Content-language: zh URI: index.en.html Content-type: text/html;charset=UTF-8 Content-language: en
当请求头为 Accept-Language: zh;q=0.9,en;q=0.8 时,中文变体的语言质量为 0.9,英文变体为 0.8。同时每个变体自身还可以通过 qs 参数(源质量值)声明默认质量,比如在目录配置中写 AddLanguage zh .zh 并配合 LanguagePriority zh en。计算时,媒体类型质量、语言质量、字符集质量、编码质量与源质量相乘,得到的结果就是该变体的最终得分。乘法规则意味着任何一个维度上得分为 0,整个变体都会被淘汰,比如客户端发送 Accept-Encoding: identity 而变体只有 gzip 压缩版本时,编码维度得分就是 0。
如果多个变体得分完全相同,Apache 会依次参考维度级别(维度越具体越优先)、LanguagePriority 指令声明的顺序,最终仍然无法决出胜负时会返回 406 Not Acceptable,或者根据 ForceNoVariant 情况选择默认变体。了解这个排序细节,有助于解释为什么有时候浏览器明明声明了偏好,返回的却是意料之外的语言版本。
在 Apache 中启用和配置内容协商
启用显式协商需要两步:加载 mod_negotiation 模块,并声明 type-map 资源类型。配置片段如下:
LoadModule negotiation_module modules/mod_negotiation.so
AddHandler type-map .var
DirectoryIndex index index.html.var
<Directory "/var/www/html">
Options MultiViews
MultiviewsMatch Any
LanguagePriority zh en
ForceLanguagePriority Prefer Fallback
</Directory>MultiViews 的作用是:当请求的文件不存在时,服务器会在同目录下寻找与请求名前缀匹配的文件,比如请求 /docs/manual 时,如果存在 manual.html、manual.html.gz、manual.zh.html 等文件,Apache 会自动执行协商。MultiviewsMatch 指令控制哪些处理器处理的文件可以参与协商,设为 Any 可以让 PHP 等脚本文件也加入候选,否则默认只有静态文件参与。
LanguagePriority 是平局裁决的关键指令,客户端没有发送 Accept-Language 头或多个语言得分相同时,服务器按该指令列出的顺序选择。ForceLanguagePriority 设为 Prefer Fallback 则表示:宁可返回 LanguagePriority 中优先的语言版本,也不轻易抛出 406 错误,这对生产环境的用户体验至关重要。
常见问题与排查思路
第一个高频问题是缓存干扰。内容协商的结果依赖请求头,而不同的客户端(桌面浏览器、爬虫、API 调用工具)发送的 Accept 头差异很大,如果反向代理或 CDN 把某个协商结果缓存下来并返回给所有客户端,就会出现语言错乱。解决方案是在响应头中启用 Vary 字段,Apache 处理协商响应时会自动附加 Vary: Accept, Accept-Language, Accept-Encoding,运维人员要确保这个头没有被代理层剥离。
第二个问题是 q 值导致 gzip 失效。一些老版本客户端会发送 Accept-Encoding: gzip;q=0 或干脆不带该头,MultiViews 目录下如果同时存在压缩和未压缩版本,协商可能选中体积更大的未压缩文件。排查时可以用 curl 模拟不同头部观察结果:
curl -H "Accept-Encoding: gzip" -I http://www.ipipp.com/index.html curl -H "Accept-Encoding: identity" -I http://www.ipipp.com/index.html curl -H "Accept-Language: en" -I http://www.ipipp.com/index
对比两次请求返回的 Content-Type、Content-Language 和 Content-Encoding,就能确认协商算法是否按预期工作。第三个值得注意的点是安全层面:MultiViews 会让服务器尝试匹配各种扩展名组合,历史上曾因此泄露隐藏文件或绕过访问控制,因此在开放 MultiViews 的目录中应谨慎管理文件命名,必要时用 RemoveHandler 或 FilesMatch 规则排除敏感文件。
总体来看,Apache 的内容协商算法并不复杂:解析请求头中的 q 值,对每个变体在四个维度上取值相乘,排序后取最优。掌握这套逻辑后,无论是搭建多语言站点、优化压缩分发,还是排查奇怪的 406 错误,都能快速定位到问题的根源。
Apache 内容协商质量因子HTTP Accept 头修改时间:2026-09-16 23:04:42