火山引擎 TOS 视频 `Content-Disposition: attachment` 排查实录

火山引擎 TOS 视频 Content-Disposition: attachment 排查实录

本文记录为什么上传时已经写了 inline、浏览器却还是 attachment ,火山 TOS 的设计动机,以及自定义域名后端代理两种解法。

文中桶名、域名、用户 ID、剧集 ID 均已替换为占位符,可直接转发。


目录

  1. 现象
  2. 时间线:我们改了什么、为什么没用
  3. 根因(三层,缺一不可)
  4. [火山 TOS 为什么这么做](#火山 TOS 为什么这么做)
  5. 修复方案(两档)
  6. 如何验证
  7. 三条媒体通路对照
  8. [避坑 Checklist](#避坑 Checklist)

1. 现象

  • 某剧集镜头视频已生成并归档至 TOS
  • 前端 <video> 能显示首帧缩略图,但点击播放无反应
  • DevTools Network 响应头示例:
http 复制代码
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. 时间线:我们改了什么、为什么没用

轮次 改动点 预期效果 实际结果
第 1~2 遍 上传时设 content_disposition="inline" 对象元数据为 inline,浏览器可播 Network 仍是 attachment
第 3 遍 预签名 URL 加 response-content-disposition=inline 签名 query 覆盖元数据 attachment(且公共直链分支根本没走到预签名)
第 4 遍 copy_object(REPLACE) 批量回填 inline 修旧对象 元数据 API 显示 inline,公网 HTTP 仍 attachment
第 5~6 遍 继续微调上传 / 预签名参数 同上 浏览器仍 attachment

关键转折 :用 TOS API head_object 与公网 curl 对比同一对象 ,发现元数据层和 HTTP 响应层不一致 ------问题不在上传,而在访问域名

对比实测(同一 mp4 文件)

探测方式 content_disposition 说明
SDK head_object inline 对象元数据已正确
默认桶域名公网 GET attachment 浏览器实际收到的头
预签名 URL + ?response-content-disposition=inline attachment query 覆盖也无效
自定义域名 公网 GET inline 见下文 §5.2

当时 .env 配置的是默认桶域名

env 复制代码
# ❌ 问题配置:走默认桶域名
TOS_PUBLIC_BASE_URL=https://<your-bucket>.tos-cn-beijing.volces.com

导致预签名逻辑直接返回公网直链、跳过预签名,前几次针对预签名的修复等于打在空气上。


3. 根因(三层,缺一不可)

复制代码
┌─────────────────────────────────────────────────────────────┐
│  Layer 1:上传元数据                                         │
│  put_object(content_disposition="inline")  ✅ 已生效         │
│  head_object 可验证                                          │
└──────────────────────────┬──────────────────────────────────┘
                           │ 默认桶域名公网读不一定会原样透出
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 2:TOS 公网边缘(默认桶域名)                          │
│  匿名 GET / Range → 强制 Content-Disposition: attachment      │
│  与对象元数据、预签名 query 均无关  ❌                         │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 3:前端播放链路                                       │
│  TOS_PUBLIC_BASE_URL = 默认桶域名 → <video> 直链 TOS  ❌     │
└─────────────────────────────────────────────────────────────┘

结论

  1. 上传没失败 ------inline 已写入对象元数据。
  2. 预签名修不了------公共直链分支 bypass 预签名;带 inline query 的签名 URL 实测仍返回 attachment。
  3. 默认桶域名不能指望 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-Disposition
head_object inline
https://<bucket>.tos-cn-beijing.volces.com/...mp4 attachment
https://<your-custom-domain>/...mp4 inline

说明:默认桶域名 下,边缘节点对 mp4 存在「元数据 inline、HTTP attachment」的覆盖行为;自定义域名则尊重对象元数据。

可能动机(推测):

推测 说明
边缘策略扩展 默认域名对可嵌入媒体统一加 attachment,文档未逐一枚举
防盗链 / 热链 默认域名易被当作公开文件直链滥用
合规分层 自定义域名视为「自有业务域名」,策略更宽松

可操作结论

不要指望在默认桶域名 上,仅靠「上传 inline」或「预签名 query」让 <video> 稳定播放。

4.3 为什么 <img> 没事、<video> 有事?

  • <img>attachment 通常仍能显示。
  • <video> 更严格,Chromium 可能拒绝 attach 响应流 →「有首帧、点播放无反应」。

参考:MDN Content-Disposition


5. 修复方案(两档)

5.1 方案 A(推荐):绑定自定义域名

步骤

  1. TOS 控制台 → 桶 → 域名管理 → 绑定自定义域名(如 https://media.example.com
  2. DNS 配置 CNAME 指向 <bucket>.tos-cn-beijing.volces.com
  3. 开启 HTTPS 证书
  4. (可选)挂火山 CDN,在「HTTP 响应头」中将 Content-Disposition 设为 inline 作双保险
  5. 修改应用配置:
env 复制代码
# ✅ 推荐:自定义域名
TOS_PUBLIC_BASE_URL=https://media.example.com

实测对比(同一对象):

bash 复制代码
# 默认桶域名 → attachment
curl -sI "https://<bucket>.tos-cn-beijing.volces.com/users/.../shot_01.mp4"

# 自定义域名 → inline
curl -sI "https://media.example.com/users/.../shot_01.mp4"

自定义域名生效后,图片和视频均可走公网直链,无需后端转发字节,减轻应用服务器压力。

代码侧注意 :若之前为兜底写了「视频强制走 /api/media 代理」,应改为仅默认桶域名才代理,自定义域名下视频允许直链:

python 复制代码
def _is_default_tos_public_host(self) -> bool:
    base = (settings.tos_public_base_url or "").rstrip("/").lower()
    default = f"https://{settings.tos_bucket}.tos-<region>.volces.com".lower()
    return base == default

def storage_ref(...):
    # 仅默认桶域名时,视频走 /api/media;自定义域名可直链
    use_public = settings.tos_public_base_url and (
        not is_video(rel) or not self._is_default_tos_public_host()
    )
    ...
typescript 复制代码
// 仅默认桶域名(*.tos-cn-beijing.volces.com)才强制代理
export const isTosDefaultBucketUrl = (url: string) =>
  url.includes('.tos-cn-beijing.volces.com/')

function videoProxyUrl(path, seriesId, token) {
  if (!isVideo(path) || !isTosDefaultBucketUrl(path)) return null
  return proxyMediaUrl(toMediaRef(path, seriesId), token)
}

5.2 方案 B(兜底):后端 /api/media 代理

若暂时无法绑自定义域名,让视频预览走应用自己的媒体 API,由后端发 inline

python 复制代码
# 默认桶域名下,视频 storage_ref 返回 /api/media/series/{id}/...
return scope.media_url(rel)
typescript 复制代码
// 默认桶域名的 mp4 → /api/media/...?token=...

代理层示例(FastAPI):

python 复制代码
return FileResponse(
    path=local_path,
    media_type="video/mp4",
    content_disposition_type="inline",  # 非 download 模式
)

5.3 上传侧(两方案都需要)

无论走哪条访问路径,上传时都应显式指定:

python 复制代码
client.put_object_from_file(
    bucket, key, file_path,
    content_type="video/mp4",
    content_disposition="inline",
    acl=tos.ACLType.ACL_Public_Read,
)

旧对象可批量 copy_object + metadata_directive="REPLACE" 回填。


6. 如何验证

自定义域名方案

  1. 硬刷新前端
  2. Network 中视频请求应为 https://media.example.com/users/.../shot_xx.mp4
  3. 响应头:Content-Disposition: inline

代理方案

  1. Network 中视频请求应为 https://your-api.example.com/api/media/series/<series_id>/...mp4?token=...
  2. 响应头:Content-Disposition: inline(或无 attachment)

自助诊断

bash 复制代码
# 1. 对象元数据(通常应为 inline)
# 使用你所用语言的 TOS SDK 调用 head_object

# 2. 默认桶域名 HTTP 头(预期 attachment --- 所以不要用来播 <video>)
curl -sI "https://<bucket>.tos-cn-beijing.volces.com/users/.../test.mp4"

# 3. 自定义域名 HTTP 头(预期 inline)
curl -sI "https://media.example.com/users/.../test.mp4"

7. 三条媒体通路对照

通路 用途 URL 形态 Content-Disposition
A 送大模型视频 API https:// / asset:// / tos:// 与浏览器无关
B 前端 <img> / <video> 自定义域名直链,或 /api/media/... 自定义域名 / 代理控制
C DB 持久化 相对路径 / tos:// / API 路径 存储引用

踩坑本质 :B 通路视频走了默认桶域名直链,而默认域名在 HTTP 层不保证 inline。


8. 避坑 Checklist

  • 上传时设 content_disposition="inline" + acl=Public_Read
  • 不要 假设 head_object=inline ⇒ 默认桶域名 GET 也是 inline
  • 生产环境优先 TOS 自定义域名TOS_PUBLIC_BASE_URL 填自定义域名而非默认桶域名
  • 仅默认桶域名时,视频走 /api/media 代理兜底
  • 排查时同时看 SDK 元数据curl 公网响应头
  • 图片可直链;视频与图片按域名策略分流
  • 可选:CDN 层将 Content-Disposition 设为 inline 双保险
  • 容器化部署:改代码后需 rebuild 镜像,仅 restart 不生效

相关链接


相关推荐
byte轻骑兵2 小时前
【LE Audio】CSIS精讲[1]: 从核心定义到协议基础,读懂多设备协同的底层逻辑
人工智能·音视频·人机交互·蓝牙耳机·le audio·低功耗蓝牙音频
中微极客3 小时前
从提示词到产品级视频:生成式AI的工程化实践与工具栈选型(LTX Studio实战)
人工智能·chatgpt·音视频
luoyayun36113 小时前
Qt + FFmpeg 视频工具:视频一键压缩功能实现
qt·ffmpeg·音视频·视频压缩
中微极客21 小时前
Kling AI 3.0:AI视频生成的工程化跃迁
人工智能·音视频
开开心心就好1 天前
免费格式转换工具视频音频文档都能转
人工智能·智能手机·ocr·电脑·音视频·散列表·启发式算法
USB_ABC1 天前
南京军区训练基地:混合矩阵赋能军事训练智慧化协同升级
矩阵·音视频
中微极客1 天前
Seedance 2.5深度解析:30秒4K视频生成与50路多模态参考的工程实践
人工智能·音视频
sweetone1 天前
一台磁带随身听的摩机(查出LAG668芯片低音差的原因)
经验分享·音视频
潜创微科技1 天前
HDMI1.3无线投屏简介
音视频