Surrogate-Control是一个容易被忽略但非常实用的HTTP响应头,它最初由RFC草案提出,专门用于控制反向代理、CDN等代理服务器的缓存行为。与Cache-Control面向浏览器不同,Surrogate-Control只对中间的代理设备生效。典型的应用场景是:内容希望边缘CDN节点缓存十分钟,但浏览器本身不要缓存,这样用户每次请求都命中最近的边缘节点,内容更新又能快速生效。本文详细分析Nginx在代理链路中如何处理这个响应头,并给出可落地的配置方案。

Surrogate-Control和Cache-Control到底有什么区别
理解这个响应头之前,先要弄清楚HTTP缓存体系中的两类对象。Cache-Control是我们最熟悉的缓存控制头,它面向的是浏览器以及标准HTTP代理,例如Cache-Control: max-age=600表示所有缓存方(包括浏览器)都可以缓存600秒。而Surrogate-Control的出现,是为了解决“想控制代理缓存但不想影响浏览器缓存”这个矛盾。
举个例子,一个电商商品详情页,如果浏览器缓存了10分钟,用户改完购物车数量后刷新页面看到的还是旧数据,体验很差。但如果完全不缓存,每次请求都回源到应用服务器,压力又很大。理想的策略是:浏览器不缓存(每次都请求),但CDN边缘节点缓存一小段时间(大部分请求命中边缘缓存)。这个需求用Cache-Control无法优雅表达,用Surrogate-Control配合一个禁止浏览器缓存的Cache-Control就能实现。
Surrogate-Control的值语法与Cache-Control类似,常见的有max-age=600,部分CDN还支持stale-while-revalidate等扩展指令。需要注意的是,它不是一个被广泛标准化的头,不同CDN厂商(如Fastly、Cloudflare、Akamai)对它的支持程度和优先级规则各有差异,使用前务必查阅对应厂商文档。
Nginx默认如何转发和处理这个响应头
很多开发者以为Nginx会自动解析Surrogate-Control并据此缓存内容,实际上默认情况下Nginx的proxy_cache机制只认它自己配置的缓存规则。当上游(比如应用服务器或更上层的CDN回源节点)返回Surrogate-Control: max-age=600时,Nginx会把这个头当作普通响应头原样透传给下游,同时自身的缓存行为完全不受它影响。
这就带来一个典型问题:如果Nginx前面还有一层自建的缓存层,或者你希望Nginx根据上游给出的Surrogate-Control指令来决定缓存时长,就需要手动解析。默认的proxy_cache_valid是全局写死的时长,无法根据不同上游响应灵活变化。虽然新版Nginx支持proxy_cache_valid按状态码区分时长,但依然读不到响应头里的值。
来看一个典型的回源链路:浏览器请求Nginx,Nginx反代到上游应用,上游同时返回Cache-Control: no-cache和Surrogate-Control: max-age=600。此时浏览器收到两个头,浏览器遵守no-cache不缓存;而如果中间有支持Surrogate-Control的CDN,CDN会缓存600秒。但如果中间那层是普通Nginx缓存,两个头都会被透传,Nginx自己的缓存判断依赖配置,可能出现该缓存的没缓存、不该透传的头被透传到浏览器的混乱情况。
在Nginx中改写和隐藏Surrogate-Control的实战配置
实际生产中,最常见的诉求有两个:一是不能把Surrogate-Control这个内部头透传给最终用户(它只对代理有意义,透传出去属于信息泄露);二是希望Nginx能利用这个头的值。第一个诉求用proxy_hide_header就能解决:
location /api/ {
proxy_pass http://backend;
# 隐藏上游返回的Surrogate-Control,不让它透传到浏览器
proxy_hide_header Surrogate-Control;
# 给浏览器明确的不缓存指令
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
第二个诉求更复杂一些:让Nginx读取上游的Surrogate-Control值并将其转换为自身的缓存时长。由于Nginx内置变量不包含任意响应头的解析,通常借助第三方模块headers-more或结合Lua实现。下面用一个常见的变通方案演示:上游约定统一返回固定格式的Surrogate-Control,Nginx侧通过map映射为不同的缓存配置。
http {
# 根据上游返回的Surrogate-Control值映射缓存时长
map $upstream_http_surrogate_control $cache_ttl {
default 0;
"max-age=60" 1m;
"max-age=600" 10m;
"max-age=3600" 1h;
}
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=edge_cache:50m
max_size=5g inactive=60m use_temp_path=off;
server {
location /content/ {
proxy_pass http://backend;
proxy_cache edge_cache;
proxy_cache_key $uri$is_args$args;
# 当映射结果为0时跳过缓存
proxy_cache_bypass $cache_ttl_zero;
proxy_no_cache $cache_ttl_zero;
# 关键:隐藏上游头,改写给浏览器的Cache-Control
proxy_hide_header Surrogate-Control;
proxy_hide_header Cache-Control;
add_header Cache-Control "no-cache";
add_header X-Cache-Status $upstream_cache_status;
}
}
}
上面配置的思路是:上游通过Surrogate-Control表达“这份内容希望代理缓存多久”,Nginx用map把它翻译成本地缓存参数,同时抹掉原始头,再给浏览器一个明确的no-cache。这样浏览器每次都请求,边缘Nginx层则按上游意愿缓存,实现了缓存分流。需要补一个变量判断映射值是否为0,例如再定义一个map $cache_ttl $cache_ttl_zero { 0 1; default 0; }来驱动proxy_cache_bypass。
回源场景下的常见坑与排查方法
第一个坑是双写冲突。有些团队让上游同时返回Cache-Control和Surrogate-Control,但两个头的值不一致,而中间某一层CDN恰好优先读Cache-Control,结果缓存行为和预期完全相反。排查时可以用curl直接回源验证:curl -sD - http://backend.example/content/1.html -o /dev/null,逐层观察每一跳实际收到的响应头,定位是哪一层改写了或忽略了头。
第二个坑是Nginx的add_header只在特定条件下生效。Nginx的add_header指令继承规则很特殊:如果当前location里写了任何add_header,父级server块里的add_header会全部失效。此外,当响应是4xx或5xx时,默认add_header也不会追加,需要加always参数。很多人配置完发现头没出现,就是这个原因。
第三个坑是缓存验证。配置完成后,建议在add_header里加上$upstream_cache_status,它的值依次可能是MISS、HIT、BYPASS、EXPIRED等,通过它观察缓存是否按预期工作。如果始终是MISS,检查proxy_cache_path路径权限、inactive参数是否小于了预期的缓存时长,以及proxy_cache_key是否在每次请求时都变化(比如不小心把query string带进了key却每次不同)。
总结
Surrogate-Control的本质是把“面向代理的缓存策略”和“面向浏览器的缓存策略”分离,这在CDN架构里非常有价值。Nginx默认不解析它,只做透传,因此在回源链路中必须显式处理:用proxy_hide_header拦截内部头避免泄露,用map加$upstream_http_surrogate_control变量把它翻译成本地缓存参数,再用add_header给浏览器下达正确的缓存指令。整套方案不依赖重写模块,纯原生配置即可完成大部分场景,配合X-Cache-Status观测头,可以构建出一套行为可控、问题可查的分层缓存体系。
NginxSurrogate-ControlCDN缓存修改时间:2026-09-07 19:40:46