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
- 先手动下载为 Buffer,用 buffer.byteLength 得到大小再走 uploadStreamAndGetVid / uploadMaterial。
- 请求时避免自动解压(axios 配置 decompress: false 或去掉 Accept-Encoding)使服务器返回原始长度。
- 若源站支持,先发 HEAD 请求获取 Content-Length;仍缺失则 fallback 到 Buffer 方案。
- 检查 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
- 优先使用直接返回静态文件的 URL,避免动态生成接口
- 下载时设置 decompress: false 或避免 Accept-Encoding 触发 chunked/压缩
- 对外部 URL 列表做 HEAD 预检,提前发现缺 content-length 的源
- 准备好 Buffer 上传兜底路径
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
- Failed to fetch image: ${response.status} ${response.statusT
- VideoUploadVidNotFound
- ChannelPlatformApiFailed
- Invalid public upload id
- Failed to fetch ${url}: ${response.status}
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/397ef10eeab2f406.
Report an issue: GitHub.