yikart/AiToEarn · error · Error

Download failed: ${response.status}

Error message

Download failed: ${response.status}

What it means

fetchWithProgress downloads a URL with fetch and throws `Download failed: ${response.status}` when response.ok is false (any non-2xx HTTP status). It surfaces the raw HTTP status because download failures are almost always server-side rejections.

Source

Thrown at project/aitoearn-web/src/utils/download.ts:18

/**
 * download.ts - 下载工具函数
 * 提供带进度回调的文件下载功能
 */

/**
 * 带进度回调的 fetch 下载
 * 通过 ReadableStream 读取响应体,实时计算下载百分比
 * 无 Content-Length 时降级为无进度下载(直接 blob)
 */
export async function fetchWithProgress(
  url: string,
  onProgress?: (progress: number) => void,
  init?: RequestInit,
): Promise<Blob> {
  const response = await fetch(url, init ?? { mode: 'no-cors' })
  if (!response.ok) {
    throw new Error(`Download failed: ${response.status}`)
  }

  const contentLength = response.headers.get('Content-Length')
  // 无 Content-Length 或无 body,降级为直接 blob
  if (!contentLength || !response.body) {
    const blob = await response.blob()
    onProgress?.(100)
    return blob
  }

  const total = Number.parseInt(contentLength, 10)
  let loaded = 0
  const reader = response.body.getReader()
  const chunks: Uint8Array[] = []

  while (true) {
    const { done, value } = await reader.read()
    if (done)

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Log response.status and re-fetch a fresh (re-signed) URL for the asset
  2. Retry with backoff for 5xx; do not retry 4xx client errors
  3. Check that the URL is reachable directly (curl -I) and that CORS/Referer rules allow the request
  4. If using mode:'no-cors', note the response is opaque — prefer a proper CORS-enabled request so status is readable

Example fix

// before
const blob = await fetchWithProgress(url)
// after
try {
  const blob = await fetchWithProgress(url)
} catch (e) {
  if (String(e.message).includes('Download failed: 4')) {
    url = await refreshSignedUrl(assetId) // get a fresh URL
    blob = await fetchWithProgress(url)
  } else throw e
}
Defensive patterns

Strategy: retry

Validate before calling

async function isUrlReachable(url: string): Promise<boolean> {
  try { const r = await fetch(url, { method: 'HEAD' }); return r.ok } catch { return false }
}

Type guard

function isOkResponse(r: Response): boolean { return r.ok }

Try / catch

try {
  const blob = await fetchWithProgress(url, onProgress)
} catch (e) {
  const m = /Download failed: (\d+)/.exec(e.message)
  if (m && Number(m[1]) >= 500) return retryWithBackoff(() => fetchWithProgress(url, onProgress))
  if (m && Number(m[1]) === 404) throw new Error('资源不存在,请刷新后重试')
  throw e
}

Prevention

When it happens

Trigger: Any fetch in fetchWithProgress (project/aitoearn-web/src/utils/download.ts) receiving 4xx/5xx — expired/signed-out media URL, 403 from hotlink/CORS policy, 404 for removed asset, 5xx from the asset server.

Common situations: Downloading media whose signed URL expired, CDN or origin returning 403/404, corporate proxy intercepting with an error page, or calling with mode:'no-cors' against a server that rejects opaque requests.

Related errors


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