{"record":{"id":"6551c74ef62a202a","repo":"yikart/AiToEarn","slug":"download-failed-response-status","errorCode":null,"errorMessage":"Download failed: ${response.status}","messagePattern":"Download failed: (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-web/src/utils/download.ts","lineNumber":18,"sourceCode":"/**\n * download.ts - 下载工具函数\n * 提供带进度回调的文件下载功能\n */\n\n/**\n * 带进度回调的 fetch 下载\n * 通过 ReadableStream 读取响应体，实时计算下载百分比\n * 无 Content-Length 时降级为无进度下载（直接 blob）\n */\nexport async function fetchWithProgress(\n  url: string,\n  onProgress?: (progress: number) => void,\n  init?: RequestInit,\n): Promise<Blob> {\n  const response = await fetch(url, init ?? { mode: 'no-cors' })\n  if (!response.ok) {\n    throw new Error(`Download failed: ${response.status}`)\n  }\n\n  const contentLength = response.headers.get('Content-Length')\n  // 无 Content-Length 或无 body，降级为直接 blob\n  if (!contentLength || !response.body) {\n    const blob = await response.blob()\n    onProgress?.(100)\n    return blob\n  }\n\n  const total = Number.parseInt(contentLength, 10)\n  let loaded = 0\n  const reader = response.body.getReader()\n  const chunks: Uint8Array[] = []\n\n  while (true) {\n    const { done, value } = await reader.read()\n    if (done)","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-web/src/utils/download.ts#L1-L36","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log response.status and re-fetch a fresh (re-signed) URL for the asset","Retry with backoff for 5xx; do not retry 4xx client errors","Check that the URL is reachable directly (curl -I) and that CORS/Referer rules allow the request","If using mode:'no-cors', note the response is opaque — prefer a proper CORS-enabled request so status is readable"],"exampleFix":"// before\nconst blob = await fetchWithProgress(url)\n// after\ntry {\n  const blob = await fetchWithProgress(url)\n} catch (e) {\n  if (String(e.message).includes('Download failed: 4')) {\n    url = await refreshSignedUrl(assetId) // get a fresh URL\n    blob = await fetchWithProgress(url)\n  } else throw e\n}","handlingStrategy":"retry","validationCode":"async function isUrlReachable(url: string): Promise<boolean> {\n  try { const r = await fetch(url, { method: 'HEAD' }); return r.ok } catch { return false }\n}","typeGuard":"function isOkResponse(r: Response): boolean { return r.ok }","tryCatchPattern":"try {\n  const blob = await fetchWithProgress(url, onProgress)\n} catch (e) {\n  const m = /Download failed: (\\d+)/.exec(e.message)\n  if (m && Number(m[1]) >= 500) return retryWithBackoff(() => fetchWithProgress(url, onProgress))\n  if (m && Number(m[1]) === 404) throw new Error('资源不存在，请刷新后重试')\n  throw e\n}","preventionTips":["Use freshly signed, short-lived URLs right before download","Retry 5xx/429 with exponential backoff; fail fast on 4xx","Verify CDN/CORS settings allow cross-origin downloads from the app domain"],"tags":["network","http","download"],"backgroundTag":"http-error-status","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}