使用TUS协议上传视频至CloudFlare时100%后报错求助
问题排查与修复方案
核心问题分析
- 后端硬编码
Upload-Length为1:CloudFlare Stream会判定文件总大小仅1字节,即便后续上传真实数据,也会被视为无效内容,最终生成空视频条目。 - 后端未处理TUS的PATCH上传请求:tus-client在获取到Location后,会向配置的endpoint发起PATCH请求以上传数据,但你的后端仅实现了创建会话的POST接口,未转发PATCH请求到CloudFlare,导致视频数据从未真正上传。
- 前端未正确对接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
相关产品推荐
相关产品推荐

