阿里云 CDN EdgeScript 配置:带 query 的 PDF 删除 Content-Disposition
环境和目标
这次要解决的是一个 PDF 下载头的问题:源站对象自带 Content-Disposition: Attachment,浏览器访问 PDF 时会下载;业务希望在 URL 带 query string 时删除这个响应头,让浏览器直接预览 PDF。
示例环境如下,真实落地时替换成自己的域名和路径:
| 项目 | 示例值 |
|---|---|
| CDN | 阿里云 CDN |
| 源站 | OSS |
| 加速域名 | static.example.com |
| PDF 路径 | /pdf/<date>/<file>.pdf |
| 预览 URL | /pdf/<date>/<file>.pdf?time=123 |
| 下载 URL | /pdf/<date>/<file>.pdf |
最终目标不是“所有 PDF 都去掉下载头”,而是下面这个精确行为:
带 query:删除 Content-Disposition,浏览器预览
不带 query:保留 Content-Disposition: Attachment,浏览器下载

这张图的关键点是:规则要放在 CDN 节点返回客户端之前执行,而不是放在 OSS 回源响应阶段。
一、先看 CloudFront 是怎么做的
CloudFront 上这类需求通常放在 Behavior 的 Function association 里:
CloudFront
→ Distributions
→ <DISTRIBUTION_ID>
→ Behaviors
→ /pdf/*/*.pdf
→ Edit
→ Function associations
→ Viewer response
Viewer response 的含义是:CloudFront 边缘节点已经拿到响应,准备返回给浏览器之前执行函数。这个阶段修改响应头,不会改源站对象,也不会把修改后的 header 写回缓存对象。
等价逻辑如下:
function handler(event) {
var request = event.request;
var response = event.response;
var querystring = request.querystring || {};
for (var key in querystring) {
if (querystring.hasOwnProperty(key)) {
delete response.headers['content-disposition'];
break;
}
}
return response;
}
这段逻辑只有一个判断:只要 URL 里有任意 query 参数,就删除 Content-Disposition。
二、阿里云 CDN 的第一个坑:不要配到回源响应头
一开始很容易误选下面这个入口:
回源配置
→ 修改入站响应头
→ 删除 Content-Disposition
这个配置看起来也能删除响应头,但它处理的是:
OSS → CDN
也就是源站返回 CDN 的阶段。
问题在于,很多 CDN 配置里 query string 不参与缓存 key。这样带 query 的请求一旦回源,并在回源响应阶段删掉 Content-Disposition,CDN 后续缓存下来的就是“没有 Content-Disposition 的对象”。结果会变成:
带 query:没有 Content-Disposition
不带 query:也没有 Content-Disposition
这和目标不一致。我们要的是“只在返回客户端前改响应头”,不是在回源阶段改缓存对象。

三、正确方案:EdgeScript 放在 foot 阶段
阿里云 CDN 里更接近 CloudFront Viewer response 的位置是:
域名管理
→ 目标加速域名
→ EdgeScript自定义策略
→ 模拟环境
→ 添加规则
规则建议先加在模拟环境,测试通过后再发布到生产环境。
规则参数:
规则名称:remove_content_disposition_for_pdf_query
执行位置:foot
优先级:10
启用:on
foot 表示在 CDN 标准配置后执行,更适合做“返回客户端前”的响应处理。
规则代码:
if and(req_uri('re:^/pdf/[^/]+/[^/]+[.]pdf$'), req_uri_query_string('re:.+')) {
del_rsp_header('Content-Disposition')
}
如果你的真实路径是 /app/pdf/<date>/<file>.pdf 这种两层结构,可以把路径正则替换成自己的目录。公开文章里不建议直接贴真实业务路径。
这段脚本做了两件事:
req_uri(...) # 限定只匹配目标 PDF 路径
req_uri_query_string('re:.+') # 限定 query string 非空
满足两个条件后,执行:
del_rsp_header('Content-Disposition')
这几个函数都来自阿里云 CDN EdgeScript 官方文档:
del_rsp_header:删除响应头req_uri:按 URI 做匹配req_uri_query_string:读取或匹配 query stringfoot:脚本执行位置之一
四、模拟环境验证
模拟环境里用同一个 PDF 测两条 URL。
带 query:
curl -I 'https://static.example.com/pdf/20260722/demo.pdf?time=123'
预期结果里没有 Content-Disposition:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 108579
X-Cache: HIT TCP_MEM_HIT
不带 query:
curl -I 'https://static.example.com/pdf/20260722/demo.pdf'
预期结果保留下载头:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: Attachment;filename=demo.pdf
X-Cache: HIT TCP_MEM_HIT
只有这两个结果同时成立,才说明规则真的符合需求。
五、发布生产和回滚点
发布前做三件事:
1. 备份当前 CDN 配置
2. 确认没有旧的回源响应头删除规则
3. EdgeScript 在模拟环境验证通过
如果之前误配过“修改入站响应头 / 回源 HTTP 响应头”,要先删除它,否则它会继续干扰缓存对象。
生产发布后再验证一遍:
# 带 query:没有 Content-Disposition
curl -I 'https://static.example.com/pdf/20260722/demo.pdf?time=123'
# 不带 query:保留 Content-Disposition
curl -I 'https://static.example.com/pdf/20260722/demo.pdf'
回滚也很直接:
EdgeScript自定义策略
→ 生产环境
→ 禁用或删除 remove_content_disposition_for_pdf_query
如果之前刷新过对象缓存,回滚后建议再刷新一次目标 PDF 路径,避免验证时看到旧缓存。
六、这次排查里的判断规则
遇到类似问题时,不要只看“某条响应头有没有了”,还要看它是在哪个阶段被删的。
几个关键判断:
origin response / 回源响应阶段:
可能改变 CDN 缓存对象里的响应头。
viewer response / 客户端响应阶段:
只改这一次返回给浏览器的响应,不污染缓存对象。
query 不参与 cache key:
不同 ?time=xxx 也可能命中同一个 CDN 缓存对象。
如果看到:
Age: 100
X-Cache: HIT TCP_MEM_HIT
说明当前结果很可能来自缓存。验证精确规则时,最好同时测试带 query 和不带 query,并在必要时刷新目标对象缓存。
快速参考
推荐配置:
入口:EdgeScript自定义策略
环境:先模拟环境,后生产环境
执行位置:foot
动作:del_rsp_header('Content-Disposition')
条件:目标 PDF 路径 + query string 非空
EdgeScript 模板:
if and(req_uri('re:^/pdf/[^/]+/[^/]+[.]pdf$'), req_uri_query_string('re:.+')) {
del_rsp_header('Content-Disposition')
}
验证标准:
带 query:没有 Content-Disposition
不带 query:有 Content-Disposition: Attachment
不要这样配:
回源配置 → 修改入站响应头 → 删除 Content-Disposition
这会在回源阶段改响应头,容易污染缓存对象,最终变成带 query 和不带 query 都可预览。

浙公网安备 33010602011771号