火山引擎 TOS 视频 `Content-Disposition: attachment` 排查实录
火山引擎 TOS 视频Content-Disposition: attachment排查实录本文记录为什么上传时已经写了inline、浏览器却还是attachment火山 TOS 的设计动机以及自定义域名与后端代理两种解法。文中桶名、域名、用户 ID、剧集 ID 均已替换为占位符可直接转发。目录现象时间线我们改了什么、为什么没用根因三层缺一不可火山 TOS 为什么这么做修复方案两档如何验证三条媒体通路对照避坑 Checklist1. 现象某剧集镜头视频已生成并归档至 TOS前端video能显示首帧缩略图但点击播放无反应DevTools Network 响应头示例HTTP/1.1 206 Partial Content Content-Type: video/mp4 Content-Disposition: attachment ← 问题所在 Content-Length: 120219 Server: TosServer对应对象路径示例users/user_id/series/series_id/episode_01/shot_01_tail_2s.mp4。2. 时间线我们改了什么、为什么没用轮次改动点预期效果实际结果第 12 遍上传时设content_dispositioninline对象元数据为 inline浏览器可播Network 仍是attachment第 3 遍预签名 URL 加response-content-dispositioninline签名 query 覆盖元数据仍attachment且公共直链分支根本没走到预签名第 4 遍copy_object(REPLACE)批量回填 inline修旧对象元数据 API 显示 inline公网 HTTP 仍 attachment第 56 遍继续微调上传 / 预签名参数同上浏览器仍 attachment关键转折用 TOS APIhead_object与公网curl对比同一对象发现元数据层和 HTTP 响应层不一致——问题不在上传而在访问域名。对比实测同一 mp4 文件探测方式content_disposition说明SDKhead_objectinline对象元数据已正确默认桶域名公网GETattachment浏览器实际收到的头预签名 URL ?response-content-dispositioninlineattachmentquery 覆盖也无效自定义域名公网GETinline✅见下文 §5.2当时.env配置的是默认桶域名# ❌ 问题配置走默认桶域名 TOS_PUBLIC_BASE_URLhttps://your-bucket.tos-cn-beijing.volces.com导致预签名逻辑直接返回公网直链、跳过预签名前几次针对预签名的修复等于打在空气上。3. 根因三层缺一不可┌─────────────────────────────────────────────────────────────┐ │ Layer 1上传元数据 │ │ put_object(content_dispositioninline) ✅ 已生效 │ │ head_object 可验证 │ └──────────────────────────┬──────────────────────────────────┘ │ 默认桶域名公网读不一定会原样透出 ▼ ┌─────────────────────────────────────────────────────────────┐ │ Layer 2TOS 公网边缘默认桶域名 │ │ 匿名 GET / Range → 强制 Content-Disposition: attachment │ │ 与对象元数据、预签名 query 均无关 ❌ │ └──────────────────────────┬──────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Layer 3前端播放链路 │ │ TOS_PUBLIC_BASE_URL 默认桶域名 → video 直链 TOS ❌ │ └─────────────────────────────────────────────────────────────┘结论上传没失败——inline已写入对象元数据。预签名修不了——公共直链分支 bypass 预签名带 inline query 的签名 URL 实测仍返回 attachment。默认桶域名不能指望 inline 播视频——必须换域名或走代理。4. 火山 TOS 为什么这么做4.1 官方公开策略默认桶域名的「安全合规」限制火山引擎自2022 年 10 月 18 日起对通过存储桶默认域名访问的网页类型文件text/html及 HTM、HTML、JSP 等在响应头中自动追加Content-Disposition: attachment防止浏览器直接渲染托管页面。官方说明静态网站托管社区同类HTML 被强制下载博客园讨论——解法包括 CDN 改响应头或绑定自定义域名。4.2 为什么 mp4 在默认桶域名也是 attachment官方文档明确列举的是 HTMLvideo/mp4不在列表中。但我们实测访问方式Content-Dispositionhead_objectinlinehttps://bucket.tos-cn-beijing.volces.com/...mp4attachmenthttps://your-custom-domain/...mp4inline✅说明默认桶域名下边缘节点对 mp4 存在「元数据 inline、HTTP attachment」的覆盖行为自定义域名则尊重对象元数据。可能动机推测推测说明边缘策略扩展默认域名对可嵌入媒体统一加 attachment文档未逐一枚举防盗链 / 热链默认域名易被当作公开文件直链滥用合规分层自定义域名视为「自有业务域名」策略更宽松可操作结论不要指望在默认桶域名上仅靠「上传 inline」或「预签名 query」让video稳定播放。4.3 为什么img没事、video有事img对attachment通常仍能显示。video更严格Chromium 可能拒绝 attach 响应流 →「有首帧、点播放无反应」。参考MDN Content-Disposition5. 修复方案两档5.1 方案 A推荐绑定自定义域名步骤TOS 控制台 → 桶 →域名管理→ 绑定自定义域名如https://media.example.comDNS 配置 CNAME 指向bucket.tos-cn-beijing.volces.com开启 HTTPS 证书可选挂火山 CDN在「HTTP 响应头」中将Content-Disposition设为inline作双保险修改应用配置# ✅ 推荐自定义域名 TOS_PUBLIC_BASE_URLhttps://media.example.com实测对比同一对象# 默认桶域名 → attachmentcurl-sIhttps://bucket.tos-cn-beijing.volces.com/users/.../shot_01.mp4# 自定义域名 → inlinecurl-sIhttps://media.example.com/users/.../shot_01.mp4自定义域名生效后图片和视频均可走公网直链无需后端转发字节减轻应用服务器压力。代码侧注意若之前为兜底写了「视频强制走/api/media代理」应改为仅默认桶域名才代理自定义域名下视频允许直链def_is_default_tos_public_host(self)-bool:base(settings.tos_public_base_urlor).rstrip(/).lower()defaultfhttps://{settings.tos_bucket}.tos-region.volces.com.lower()returnbasedefaultdefstorage_ref(...):# 仅默认桶域名时视频走 /api/media自定义域名可直链use_publicsettings.tos_public_base_urland(notis_video(rel)ornotself._is_default_tos_public_host())...// 仅默认桶域名*.tos-cn-beijing.volces.com才强制代理exportconstisTosDefaultBucketUrl(url:string)url.includes(.tos-cn-beijing.volces.com/)functionvideoProxyUrl(path,seriesId,token){if(!isVideo(path)||!isTosDefaultBucketUrl(path))returnnullreturnproxyMediaUrl(toMediaRef(path,seriesId),token)}5.2 方案 B兜底后端/api/media代理若暂时无法绑自定义域名让视频预览走应用自己的媒体 API由后端发inline# 默认桶域名下视频 storage_ref 返回 /api/media/series/{id}/...returnscope.media_url(rel)// 默认桶域名的 mp4 → /api/media/...?token...代理层示例FastAPIreturnFileResponse(pathlocal_path,media_typevideo/mp4,content_disposition_typeinline,# 非 download 模式)5.3 上传侧两方案都需要无论走哪条访问路径上传时都应显式指定client.put_object_from_file(bucket,key,file_path,content_typevideo/mp4,content_dispositioninline,acltos.ACLType.ACL_Public_Read,)旧对象可批量copy_objectmetadata_directiveREPLACE回填。6. 如何验证自定义域名方案硬刷新前端Network 中视频请求应为https://media.example.com/users/.../shot_xx.mp4响应头Content-Disposition: inline代理方案Network 中视频请求应为https://your-api.example.com/api/media/series/series_id/...mp4?token...响应头Content-Disposition: inline或无 attachment自助诊断# 1. 对象元数据通常应为 inline# 使用你所用语言的 TOS SDK 调用 head_object# 2. 默认桶域名 HTTP 头预期 attachment — 所以不要用来播 videocurl-sIhttps://bucket.tos-cn-beijing.volces.com/users/.../test.mp4# 3. 自定义域名 HTTP 头预期 inlinecurl-sIhttps://media.example.com/users/.../test.mp47. 三条媒体通路对照通路用途URL 形态Content-DispositionA送大模型视频 APIhttps:///asset:///tos://与浏览器无关B前端img/video自定义域名直链或/api/media/...自定义域名 / 代理控制CDB 持久化相对路径 /tos:/// API 路径存储引用踩坑本质B 通路视频走了默认桶域名直链而默认域名在 HTTP 层不保证 inline。8. 避坑 Checklist上传时设content_dispositioninlineaclPublic_Read不要假设head_objectinline⇒ 默认桶域名 GET 也是 inline生产环境优先TOS 自定义域名TOS_PUBLIC_BASE_URL填自定义域名而非默认桶域名仅默认桶域名时视频走/api/media代理兜底排查时同时看SDK 元数据和curl 公网响应头图片可直链视频与图片按域名策略分流可选CDN 层将Content-Disposition设为inline双保险容器化部署改代码后需rebuild 镜像仅 restart 不生效相关链接火山 TOS 静态网站 / 默认域名策略火山 CDN HTTP 响应头设置MDN Content-Disposition