火山引擎 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?

官方文档明确列举的是 HTML ;video/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 不生效

相关链接


相关推荐
TechVoyager_82467 小时前
多形态输入与12 通道音频:IT6615 这颗 HDMI 发射器的接口宽度
音视频·音频芯片·芯片选型·视频发射
Raymond16887 小时前
AI配音工具长文本合成架构技术解析:全局音色锚定与声学状态跟踪
人工智能·架构·实时互动·视觉检测·音视频·语音识别
hflmwl814 小时前
Wise Data Recovery找回误删的文档照片音乐和视频
音视频·datarecovery·找回误删的文档照片音乐和视频·找回误删的文档照片音乐视频
技灵AI14 小时前
AI 视频转场提示词怎么写?让前后画面衔接更自然
人工智能·prompt·aigc·音视频
youandyouyou16 小时前
跨区培训的屏幕共享怎么做才稳?同屏同步与弱网优化实践
音视频·网络通信
开开心心就好16 小时前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
美狐美颜SDK开放平台16 小时前
直播APP集成美颜SDK,如何梳理第三方组件的数据权限?
开发语言·人工智能·深度学习·音视频·第三方美颜sdk
l143723326718 小时前
批量视频硬字幕擦除多少钱?影响报价的关键因素全解析
音视频
重生六道18 小时前
B站视频时长统计:一段JS脚本搞定分P/合集总时长(附倍速计算)
开发语言·javascript·音视频
音视频牛哥18 小时前
不换摄像机,也能接入 WebRTC?SmartMediaKit RTSP/RTMP 转 WHIP 技术实践
音视频·webrtc·smartmediakit·rtsp转whip·rtmp转whip·whip推流·rtsp转webrtc播放