按请求头协商语言的站点加了缓存,译文为什么会发给用另一种语言的人

📅 2026-10-11 👁️ 0

按请求头协商语言的站点加了缓存之后,译文发给用另一种语言的访客,成因几乎总是同一个:缓存键里只有地址和方法,没有语言这个影响因素。MDN 对 Vary 的定义正好说明这一点:该响应头描述的是请求信息中除方法与 URL 之外、影响响应内容的那部分;包含 Vary 头可确保响应依据所列首部字段分别被缓存,最常见的用途便是在内容协商时构造缓存键。没有这个声明,中间层会把第一位访客拿到的译文当作该地址的标准答案。

缓存键默认只看地址与方法

缓存判断「是否命中」的依据是缓存键。默认情况下它由请求方法与目标地址组成,最多再加**问路径的某些片段。

内容协商破坏了这一前提。同一个地址,因为请求头里 Accept-Language 的取值不同,服务器生成完全不同的正文。从缓存的视角看却是两次相同键的请求,于是第二次直接复用第一次的响应。

这类问题的特征很稳定:本地直连源站正常,接入缓存或加速节点后出现;清一次缓存后短时间内正常,随后又开始串;不同浏览器设置的人看到同一种语言。出现这三条中的任意两条,就应优先怀疑缓存键,而不是翻译逻辑。

协商语言时缺了什么

缺的不是翻译能力,而是「把影响因素写进响应」。

服务端在决定返回哪种语言时,读取了请求头;但响应本身并没有告诉下游「我的内容取决于那个头」。这是一次单向的信息丢失:程序知道,缓存不知道。

Vary 就是补上这条信息的机制。把它写成包含语言相关首部之后,缓存会按该字段的不同取值分别存副本,命中率会下降,但不会错发。

还有一个更彻底的取值:Vary 取星号时,意味着有其他因素影响该响应的生成,等价于该响应不可缓存。这个写法诚实但代价大,一般只在确实无法界定影响因素时使用。

需要特别注意的是取值一致性。同一 URL 的所有响应都应使用相同的 Vary 值,包括未修改响应与默认或错误响应。只在成功页面写、在错误页不写,会让缓存里同时存在两种声明的对象,行为比不写更难预期。

响应头里该怎么补

落地时按三步写。

第一步,凡是依据语言首部生成正文的地址,都补上语言相关的声明。若同时对压缩方式做协商,两者要一起列出,因为分别命中错误版本的原因是一样的。

第二步,把协商规则固定下来。语言匹配应当有明确顺序,例如精确语言优先、同语系次选、最后落到默认语言,而不是「看哪个头先来」。规则不稳定时,即使缓存正确,同一用户两次访问也可能拿到不同译文。

第三步,给可选语言留出显式入口。依赖请求头做判断,用户其实无法主动选择;提供可点击的语言切换并让选择结果落到可寻址的地址上,比纯协商更容易被缓存与抓取方正确处理。

多语言站点的两种排法

站内支持多语言站点配置与切换,并支持整页 HTML 翻译。实现上通常有两条路线,它们与缓存的关系不同。

一条是按请求头协商,同一地址返回不同语言版本。它的好处是地址简短、结构不用扩张;代价就是本文讨论的这类缓存问题,以及对抓取方而言,同一地址对应多种内容带来的歧义。

另一条是按语言分址,例如用子目录或子域名把不同语言隔开。它的地址与内容一一对应,缓存几乎不会出错,代价是站点结构变大,翻译内容的维护量随之增加。

整页翻译这类由系统生成译文的能力,还会引入第三种情况:译文与原页可能同址也可能分址,取决于实现。部署时要确认译文产物走的是哪条链路——若译文被写成静态文件放在同一地址之前,缓存与文件层的优先级会盖过协商逻辑,排查时容易看错方向。

自建站与托管环境在这点上差别明显:自己部署时可以同时控制协商规则与缓存层配置;托管场景常常只能改站点侧,动不了中间层的缓存键策略,此时更应倾向分址方案。

验证顺序

按这个顺序做,能在几分钟内定位问题。

先直连源站,用两种语言首部分别请求同一地址,确认响应确实不同,并且两条响应都带有语言影响因素的声明。再穿过缓存层重复同样的两次请求,确认各自命中自己的副本而不是同一份。最后核对未修改响应与错误响应,确保它们的声明与正常响应一致。

如果第二步出现串版本,处理方式按优先级排列:补声明、把该地址改为不可缓存、或迁移到分址方案。三者对命中率的影响依次增大,但确定性也依次增大。

常见问题

加了声明后命中率下降怎么办? 这是正确行为带来的必然代价。可以把与语言无关的资源(样式、图片)单独放址,让它们继续享受高命中。

搜索引擎抓到的是哪种语言? 抓取方的请求头通常偏向默认语言。若站点面向多语言市场,分址更可控。

Cookie 里的语言偏好能不能替代请求头? 可以参与判断,但会让缓存问题更严重:个性化凭据带来的响应差异同样需要在缓存与声明层面处理。

为什么有时刷新一下又对了? 缓存对象过期或被替换时会出现这种间歇表现,不能作为问题已解决的依据,仍应按验证顺序复核。

整页翻译生成的内容需要单独处理吗? 需要确认它与原页是否同址。同址就要走协商声明那条路;分址则要核对新地址的缓存与站点地图覆盖。

相关文章

登录后的页面被共享缓存存了一份,缓存头里的私有与不可存该怎么写

MDN 的 Cache-Control 文档写明 no-store 表示任何类型的缓存都不应存储该响应,private 表示只能存于私有缓存,并提示共享缓存会把一份响应复用给多个用户,个性化内容不应交给它。本文说明两者边界、会员分层页面为什么更容易出问题。

2026-10-11

上传目录的类型头写对了还不行,禁止内容嗅探的响应头为什么要单独加

MDN 文档写明 X-Content-Type-Options 表示 Content-Type 里声明的类型应当被遵守而不应被更改,nosniff 会在脚本或样式请求的类型不符时阻止响应。本文说明嗅探发生在哪一步、上传目录为什么要在部署层再挡一道,以及站内两层各管什么。

2026-10-11

HTTP/3 要在 Web 服务器里打开,端口、证书和编译选项各要动哪一处

Nginx 官方模块文档写明 ngx_http_v3_module 自 1.25.0 起提供实验性 HTTP/3 支持且默认不编译,需要 --with-http_v3_module 启用,示例监听端口建议与 HTTPS 一致,0-RTT 还要求 OpenSSL 3.5.1 或更高。本文按编译、监听、证书、库版本四步说明启用顺序。

2026-10-11

开源协议写明是 GPL 时,改版和交付给客户要注意什么

以 WordPress 官方授权页为依据,GPLv2 或更新版本的要求覆盖衍生作品,插件与主题被官方视为衍生作品,同时存在法律灰区。本文说明对外交付时要带的三样东西、多站点与二次开发的分界,以及选型阶段的授权核对顺序。

2026-10-11

站点升级时挂维护页返回 503,为什么比返回空白页更稳妥

MDN 的 503 文档写明该状态码表示服务器未准备好处理请求,常见原因是维护或过载,响应应仅用于临时状况并尽可能带上预计恢复时间的 Retry-After 首部;文档同时提示 503 表示临时问题,通常不应缓存。本文说明维护窗口里状态码、重试时间与缓存该怎么配。

2026-10-11

页面其实存在却一直返回 404,收录会受到什么影响

内容还在库里,前台却把页面判成不存在,搜索引擎收到的信号是资源消失,收录与流量都会跟着掉。本文说明统一错误码为什么危险,状态码按语义分流的几种情况,以及文档状态、伪静态规则、重定向和站点地图四处配置的排查顺序。

2026-10-11

登录成功后的跳转地址由参数带来,不校验会被拿去做什么

登录回跳参数是外部可控输入,不做集中校验时,一个合法的登录流程会被改造成可信域到钓鱼页的跳板。本文说明这类跳转地址会被拿去做什么、白名单该挡哪几类取值,以及验证码与频率控制在登录链路上的分工。

2026-10-11

矢量图标能带脚本,上传图片时净化该挡掉哪些内容

矢量图标不是图片位的数据,而是一种可包含脚本元素的文档格式。上传时做净化要挡掉脚本与事件属性、外部引用与超大解压体积,同时保留正常展示内容。本文给出净化清单与黑名单、白名单两种策略的取舍。

2026-10-11