You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用TUS协议上传视频至CloudFlare时100%后报错求助

问题排查与修复方案

核心问题分析

  1. 后端硬编码Upload-Length为1:CloudFlare Stream会判定文件总大小仅1字节,即便后续上传真实数据,也会被视为无效内容,最终生成空视频条目。
  2. 后端未处理TUS的PATCH上传请求:tus-client在获取到Location后,会向配置的endpoint发起PATCH请求以上传数据,但你的后端仅实现了创建会话的POST接口,未转发PATCH请求到CloudFlare,导致视频数据从未真正上传。
  3. 前端未正确对接CloudFlare上传URL:tus-client默认基于配置的endpoint发起所有请求,需手动指定上传目标为后端返回的CloudFlare地址。

具体修复方案

方案一:后端代理所有TUS请求(推荐,鉴权逻辑保留在后端)

1. 更新后端代码,支持POST/PATCH/OPTIONS请求

// 处理TUS创建会话、上传数据及预检请求
router.all('/oneTimeLink', async (req, res) => {
  try {
    // 获取前端传递的TUS头信息
    const tusResumable = req.headers['tus-resumable'] || '1.0.0';
    const uploadLength = req.headers['upload-length'];
    const clientMetadata = req.headers['upload-metadata'];
    
    // 合并自定义metadata与前端传递的metadata
    let uploadMetadata = 'maxdurationseconds NjAw';
    if (clientMetadata) uploadMetadata += ',' + clientMetadata;

    if (req.method === 'POST') {
      // 向CloudFlare发起创建上传会话请求
      const cfResponse = await axios({
        method: 'POST',
        url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_CLIENT_ID}/stream?direct_user=true`,
        headers: {
          'Authorization': `Bearer ${process.env.CLOUDFLARE_KEY_ID}`,
          'Tus-Resumable': tusResumable,
          'Upload-Length': uploadLength,
          'Upload-Metadata': uploadMetadata,
        },
      });

      const location = cfResponse.headers.location;
      res.set({
        'Access-Control-Allow-Headers': '*',
        'Access-Control-Expose-Headers': '*',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Tus-Resumable': tusResumable,
        'Location': location,
      });
      res.sendStatus(201); // TUS标准创建会话成功状态码
    } else if (req.method === 'PATCH') {
      // 从请求头提取CloudFlare上传URL,转发PATCH上传请求
      const cfUploadUrl = req.headers['location'];
      const cfResponse = await axios({
        method: 'PATCH',
        url: cfUploadUrl,
        headers: {
          'Authorization': `Bearer ${process.env.CLOUDFLARE_KEY_ID}`,
          'Tus-Resumable': tusResumable,
          'Upload-Offset': req.headers['upload-offset'],
          'Content-Type': 'application/offset+octet-stream',
        },
        data: req.body,
      });

      res.set({
        'Access-Control-Allow-Headers': '*',
        'Access-Control-Expose-Headers': '*',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Tus-Resumable': tusResumable,
        'Upload-Offset': cfResponse.headers['upload-offset'],
      });
      res.sendStatus(cfResponse.status);
    } else if (req.method === 'OPTIONS') {
      // 处理CORS预检请求
      res.set({
        'Access-Control-Allow-Headers': '*',
        'Access-Control-Expose-Headers': '*',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
      });
      res.sendStatus(200);
    }
  } catch (error) {
    console.error(error);
    res.sendStatus(500);
  }
});

2. 前端代码无需修改

现有前端代码已能自动传递文件大小与metadata,保持原配置即可。


方案二:前端直接使用CloudFlare上传URL(简化后端逻辑)

1. 修改后端代码,返回CloudFlare上传URL

router.post('/oneTimeLink', async (req, res) => {
  try {
    const { uploadLength, metadata } = req.body;
    // 将metadata值编码为base64(TUS协议要求)
    const encodedFilename = Buffer.from(metadata.filename).toString('base64');
    const encodedFiletype = Buffer.from(metadata.filetype).toString('base64');
    const uploadMetadata = `maxdurationseconds NjAw,filename ${encodedFilename},filetype ${encodedFiletype}`;

    const cfResponse = await axios({
      method: 'POST',
      url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_CLIENT_ID}/stream?direct_user=true`,
      headers: {
        'Authorization': `Bearer ${process.env.CLOUDFLARE_KEY_ID}`,
        'Tus-Resumable': '1.0.0',
        'Upload-Length': uploadLength,
        'Upload-Metadata': uploadMetadata,
      },
    });

    res.set({
      'Access-Control-Allow-Headers': '*',
      'Access-Control-Expose-Headers': '*',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': '*',
    });
    res.json({ uploadUrl: cfResponse.headers.location });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: '生成上传链接失败' });
  }
});

2. 更新前端代码,先获取上传URL再初始化TUS上传

import * as tus from "tus-js-client";

const file = state.coursesData['Module 1'][0].content.video;

// 先请求后端获取CloudFlare上传URL
fetch('http://localhost:8080/dev/coursesLibraryPractitioner/oneTimeLink', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    uploadLength: file.size,
    metadata: {
      filename: file.name,
      filetype: file.type,
    },
  }),
})
.then(res => res.json())
.then(data => {
  const upload = new tus.Upload(file, {
    endpoint: data.uploadUrl, // 直接指向CloudFlare上传地址
    chunkSize: 250 * 1024 * 1024,
    onError: error => console.log("上传失败: " + error),
    onProgress: (bytesUploaded, bytesTotal) => {
      const percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
      console.log(bytesUploaded, bytesTotal, `${percentage}%`);
    },
    onSuccess: () => console.log("上传成功,可访问地址: ", upload.url),
  });
  upload.start();
})
.catch(err => console.error("获取上传链接失败: ", err));

额外注意事项

  • 确保CloudFlare API密钥具备Stream上传权限。
  • 方案二需在CloudFlare控制台配置CORS,允许前端域名发起GET/POST/PATCH/OPTIONS请求,并放行TUS相关请求头。
  • 禁止硬编码Upload-Length,必须使用真实文件大小,否则CloudFlare会拒绝完整上传内容。

内容的提问来源于stack exchange,提问作者RafaelEchart

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 22:40:35