yikart/AiToEarn · error · AppException

VideoUploadFailed

VideoUploadFailed

Error message

无法获取视频文件大小(响应头缺少 content-length)

What it means

downloadUrlAndUploadAsStream 用 axios 以 stream 方式下载视频 URL,随后必须把 FileSize 传给火山 VOD UploadMaterial;当响应头缺少 content-length(例如源站使用 chunked 传输编码、重定向后头丢失、压缩响应)时无法确定文件大小,库直接抛出 VideoUploadFailed。

Source

Thrown at project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/volcengine/services/upload.service.ts:189

   * @returns 上传成功后的 VID
   */
  async downloadUrlAndUploadAsStream(
    url: string,
    options?: VideoUrlInput,
  ): Promise<string> {
    let fileName = options?.fileName
    let fileExtension = options?.fileExtension

    const response: AxiosResponse<NodeJS.ReadableStream> = await axios.get(url, {
      responseType: 'stream',
      timeout: 60000,
      maxContentLength: 1000 * 1024 * 1024,
      maxBodyLength: 1000 * 1024 * 1024,
    })

    const contentLength = response.headers['content-length']
    if (!contentLength) {
      throw new AppException(ResponseCode.VideoUploadFailed, {
        message: '无法获取视频文件大小(响应头缺少 content-length)',
      })
    }
    const fileSize = Number.parseInt(contentLength, 10)

    if (!fileName || !fileExtension) {
      const urlObj = new URL(url)
      const pathname = urlObj.pathname
      const fileNameFromUrl = pathname.split('/').pop() || 'video'

      if (!fileExtension) {
        fileExtension = path.extname(fileNameFromUrl).toLowerCase() || '.mp4'
      }
      else if (!fileExtension.startsWith('.')) {
        fileExtension = `.${fileExtension}`
      }

      if (!fileName) {

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. 先手动下载为 Buffer,用 buffer.byteLength 得到大小再走 uploadStreamAndGetVid / uploadMaterial。
  2. 请求时避免自动解压(axios 配置 decompress: false 或去掉 Accept-Encoding)使服务器返回原始长度。
  3. 若源站支持,先发 HEAD 请求获取 Content-Length;仍缺失则 fallback 到 Buffer 方案。
  4. 检查 URL 是否经过会改变传输方式的代理,必要时直连源站或换用 uploadMediaByUrl 让火山侧直接拉取。

Example fix

// before
const vid = await uploadService.downloadUrlAndUploadAsStream(url)
// after
const { data } = await axios.get(url, { responseType: 'arraybuffer', maxContentLength: 1000 * 1024 * 1024 })
const vid = await uploadService.uploadStreamAndGetVid({
  stream: Buffer.from(data),
  fileSize: data.byteLength,
  fileName: 'video',
  fileExtension: '.mp4',
})
Defensive patterns

Strategy: fallback

Validate before calling

async function assertHasContentLength(url: string): Promise<void> {
  const res = await fetch(url, { method: 'HEAD' })
  if (!res.headers.get('content-length')) {
    logger.warn({ url }, 'No content-length; use buffer download fallback')
  }
}

Type guard

function hasContentLength(h: Record<string, unknown>): h is Record<string, string> & { 'content-length': string } {
  return typeof h['content-length'] === 'string' && !Number.isNaN(Number.parseInt(h['content-length'], 10))
}

Try / catch

try {
  vid = await uploadService.downloadUrlAndUploadAsStream(url)
} catch (e) {
  if (isVideoUploadFailed(e) && String(e.message).includes('content-length')) {
    const { data } = await axios.get(url, { responseType: 'arraybuffer' })
    vid = await uploadService.uploadStreamAndGetVid({
      stream: Buffer.from(data), fileSize: data.byteLength, fileName: 'video', fileExtension: '.mp4',
    })
  } else { throw e }
}

Prevention

When it happens

Trigger: 对 downloadUrlAndUploadAsStream(url) 传入的 URL 响应没有 content-length 头:chunked 分块传输、gzip/br 压缩响应、某些 CDN/预签名 URL(如 OSS/S3 未设置 Content-Length)、HEAD 与 GET 响应不一致。

Common situations: 源站为动态生成视频的接口而非静态文件;代理或 CDN 剥掉了 content-length;重定向到不支持该头的存储;HTTPS 中间层重新分块。

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/397ef10eeab2f406. Report an issue: GitHub.