{"record":{"id":"245a320be4124b47","repo":"vercel-labs/skills","slug":"download-failed-with-http-response-status","errorCode":null,"errorMessage":"Download failed with HTTP ${response.status}","messagePattern":"Download failed with HTTP (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/download-source.ts","lineNumber":91,"sourceCode":"  if (state.bytes > limits.extractMaxBytes) {\n    throw new ArchiveValidationError(\n      `Archive extracts to more than ${limits.extractMaxBytes} bytes. Set SKILLS_EXTRACT_MAX_BYTES to override.`\n    );\n  }\n}\n\nasync function downloadToFile(\n  url: string,\n  targetFile: string,\n  limits: DownloadLimits\n): Promise<void> {\n  const response = await fetch(url, {\n    signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n    redirect: 'follow',\n  });\n\n  if (!response.ok) {\n    throw new Error(`Download failed with HTTP ${response.status}`);\n  }\n\n  const contentLength = response.headers.get('content-length');\n  if (contentLength) {\n    const parsed = Number.parseInt(contentLength, 10);\n    if (Number.isFinite(parsed) && parsed > limits.downloadMaxBytes) {\n      throw new Error(\n        `Download is larger than ${limits.downloadMaxBytes} bytes. Set SKILLS_DOWNLOAD_MAX_BYTES to override.`\n      );\n    }\n  }\n\n  if (!response.body) {\n    throw new Error('Download response has no body');\n  }\n\n  let downloaded = 0;\n  const limitStream = new TransformStream<Uint8Array, Uint8Array>({","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/vercel-labs/skills/blob/435076e78988e1e6ec40d00b0b1d76bdbbc5419a/src/download-source.ts#L73-L109","documentation":"downloadToFile issues a fetch with redirect:'follow' and a timeout, and throws when response.ok is false — any HTTP status outside 2xx aborts the download before it starts. The status code is embedded in the message, so a 404 means the URL is wrong, 403/401 means auth or rate limiting, and 5xx means the origin failed.","triggerScenarios":"Calling downloadSource with a URL that returns a non-2xx status: deleted GitHub release asset (404), private repo without a token (403/401), rate-limited API (403/429), or an expired pre-signed URL.","commonSituations":"Typos or stale URLs pointing to moved/renamed releases, GitHub rate limits hit in CI, private repositories downloaded without GITHUB_TOKEN/GH_TOKEN set, or link rot in documentation.","solutions":["curl -I the URL to see the actual status and confirm it's reachable","Fix the URL (correct tag/release/branch) or make the repo/asset public","For private GitHub resources, export GITHUB_TOKEN or GH_TOKEN before running","If rate-limited (403/429), wait or supply a token; retry with backoff in scripts"],"exampleFix":"# before\nskills add https://github.com/acme/skills/archive/refs/heads/main.zip  # HTTP 404\n\n# after\ncurl -I https://github.com/acme/skills/archive/refs/heads/main.zip  # check\nskills add acme/skills   # install from git source instead","handlingStrategy":"retry","validationCode":"// Probe the URL before downloading\nasync function assertDownloadable(url: string): Promise<void> {\n  const res = await fetch(url, { method: 'HEAD' });\n  if (!res.ok) throw new Error(`URL not downloadable: HTTP ${res.status}`);\n}","typeGuard":"null","tryCatchPattern":"for (let attempt = 1; attempt <= 3; attempt++) {\n  try {\n    return await downloadSource(url);\n  } catch (err) {\n    const msg = err instanceof Error ? err.message : '';\n    if (!/^Download failed with HTTP \\d+$/.test(msg) || attempt === 3) throw err;\n    await new Promise(r => setTimeout(r, attempt * 1000));\n  }\n}","preventionTips":["Pin URLs to immutable release tags/SHAs, not moving branches","Set GITHUB_TOKEN/GH_TOKEN in CI to avoid 403 rate limits"],"tags":["network","http","download","status-code"],"backgroundTag":"http-error-status","analyzedSha":"435076e78988e1e6ec40d00b0b1d76bdbbc5419a","analyzedAt":"2026-08-28T17:47:53.369Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}