{"record":{"id":"397ef10eeab2f406","repo":"yikart/AiToEarn","slug":"videouploadfailed","errorCode":"VideoUploadFailed","errorMessage":"无法获取视频文件大小（响应头缺少 content-length）","messagePattern":"无法获取视频文件大小（响应头缺少 content-length）","errorType":"error_code","errorClass":"AppException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/volcengine/services/upload.service.ts","lineNumber":189,"sourceCode":"   * @returns 上传成功后的 VID\n   */\n  async downloadUrlAndUploadAsStream(\n    url: string,\n    options?: VideoUrlInput,\n  ): Promise<string> {\n    let fileName = options?.fileName\n    let fileExtension = options?.fileExtension\n\n    const response: AxiosResponse<NodeJS.ReadableStream> = await axios.get(url, {\n      responseType: 'stream',\n      timeout: 60000,\n      maxContentLength: 1000 * 1024 * 1024,\n      maxBodyLength: 1000 * 1024 * 1024,\n    })\n\n    const contentLength = response.headers['content-length']\n    if (!contentLength) {\n      throw new AppException(ResponseCode.VideoUploadFailed, {\n        message: '无法获取视频文件大小（响应头缺少 content-length）',\n      })\n    }\n    const fileSize = Number.parseInt(contentLength, 10)\n\n    if (!fileName || !fileExtension) {\n      const urlObj = new URL(url)\n      const pathname = urlObj.pathname\n      const fileNameFromUrl = pathname.split('/').pop() || 'video'\n\n      if (!fileExtension) {\n        fileExtension = path.extname(fileNameFromUrl).toLowerCase() || '.mp4'\n      }\n      else if (!fileExtension.startsWith('.')) {\n        fileExtension = `.${fileExtension}`\n      }\n\n      if (!fileName) {","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/volcengine/services/upload.service.ts#L171-L207","documentation":"downloadUrlAndUploadAsStream 用 axios 以 stream 方式下载视频 URL，随后必须把 FileSize 传给火山 VOD UploadMaterial；当响应头缺少 content-length（例如源站使用 chunked 传输编码、重定向后头丢失、压缩响应）时无法确定文件大小，库直接抛出 VideoUploadFailed。","triggerScenarios":"对 downloadUrlAndUploadAsStream(url) 传入的 URL 响应没有 content-length 头：chunked 分块传输、gzip/br 压缩响应、某些 CDN/预签名 URL（如 OSS/S3 未设置 Content-Length）、HEAD 与 GET 响应不一致。","commonSituations":"源站为动态生成视频的接口而非静态文件；代理或 CDN 剥掉了 content-length；重定向到不支持该头的存储；HTTPS 中间层重新分块。","solutions":["先手动下载为 Buffer，用 buffer.byteLength 得到大小再走 uploadStreamAndGetVid / uploadMaterial。","请求时避免自动解压（axios 配置 decompress: false 或去掉 Accept-Encoding）使服务器返回原始长度。","若源站支持，先发 HEAD 请求获取 Content-Length；仍缺失则 fallback 到 Buffer 方案。","检查 URL 是否经过会改变传输方式的代理，必要时直连源站或换用 uploadMediaByUrl 让火山侧直接拉取。"],"exampleFix":"// before\nconst vid = await uploadService.downloadUrlAndUploadAsStream(url)\n// after\nconst { data } = await axios.get(url, { responseType: 'arraybuffer', maxContentLength: 1000 * 1024 * 1024 })\nconst vid = await uploadService.uploadStreamAndGetVid({\n  stream: Buffer.from(data),\n  fileSize: data.byteLength,\n  fileName: 'video',\n  fileExtension: '.mp4',\n})","handlingStrategy":"fallback","validationCode":"async function assertHasContentLength(url: string): Promise<void> {\n  const res = await fetch(url, { method: 'HEAD' })\n  if (!res.headers.get('content-length')) {\n    logger.warn({ url }, 'No content-length; use buffer download fallback')\n  }\n}","typeGuard":"function hasContentLength(h: Record<string, unknown>): h is Record<string, string> & { 'content-length': string } {\n  return typeof h['content-length'] === 'string' && !Number.isNaN(Number.parseInt(h['content-length'], 10))\n}","tryCatchPattern":"try {\n  vid = await uploadService.downloadUrlAndUploadAsStream(url)\n} catch (e) {\n  if (isVideoUploadFailed(e) && String(e.message).includes('content-length')) {\n    const { data } = await axios.get(url, { responseType: 'arraybuffer' })\n    vid = await uploadService.uploadStreamAndGetVid({\n      stream: Buffer.from(data), fileSize: data.byteLength, fileName: 'video', fileExtension: '.mp4',\n    })\n  } else { throw e }\n}","preventionTips":["优先使用直接返回静态文件的 URL，避免动态生成接口","下载时设置 decompress: false 或避免 Accept-Encoding 触发 chunked/压缩","对外部 URL 列表做 HEAD 预检，提前发现缺 content-length 的源","准备好 Buffer 上传兜底路径"],"tags":["http","video-upload","content-length","volcengine-vod"],"backgroundTag":"missing-content-length","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}