{"record":{"id":"19657254751fcb4b","repo":"paperclipai/paperclip","slug":"artifact-download-failed-http-response-status","errorCode":null,"errorMessage":"Artifact download failed: HTTP ${response.status}","messagePattern":"Artifact download failed: HTTP (.+?)","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloud-migrator-artifacts.mjs","lineNumber":137,"sourceCode":"    for (const name of names) {\n      copyFileSync(path.join(directory, `${name}.tgz`), path.join(scratch, `${name}.tgz`));\n      // The public objects do not exist yet. Only transport changes for this\n      // smoke install; exact versions, integrity, root and transitive pins stay.\n      lock.packages[`node_modules/@paperclipai/${name}`].resolved = `file:${name}.tgz`;\n    }\n    writeFileSync(path.join(scratch, \"package.json\"), JSON.stringify({ name: \"paperclip-migrator-install-root\", version: \"0.0.0\", private: true,\n      dependencies: { \"@paperclipai/db\": manifest.packageVersion } }));\n    writeFileSync(path.join(scratch, \"package-lock.json\"), JSON.stringify(lock));\n    exec(\"npm\", [\"ci\", \"--ignore-scripts\", \"--no-audit\", \"--no-fund\", \"--update-notifier=false\", \"--cache\", path.join(scratch, \"empty-cache\"),\n      \"--registry=https://registry.npmjs.org\"], { cwd: scratch, stdio: \"inherit\", timeout: 180_000 });\n    for (const name of names) assertMetadata(JSON.parse(readFileSync(path.join(scratch, \"node_modules\", \"@paperclipai\", name, \"package.json\"), \"utf8\")), `@paperclipai/${name}`, sha);\n    exec(process.execPath, [\"--input-type=module\", \"--eval\", \"await import('@paperclipai/db'); await import('@paperclipai/shared');\"], { cwd: scratch, stdio: \"inherit\", timeout: 30_000 });\n  } finally { rmSync(scratch, { recursive: true, force: true }); }\n}\n\nasync function download(url, fetchImpl) {\n  const response = await fetchImpl(url, { redirect: \"error\", signal: AbortSignal.timeout(60_000) });\n  if (!response.ok) throw new Error(`Artifact download failed: HTTP ${response.status}`, { cause: { status: response.status } });\n  const reader = response.body.getReader();\n  const chunks = [];\n  let size = 0;\n  try {\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      size += value.length;\n      if (size > maximumBytes) throw new Error(\"Artifact exceeds size limit.\");\n      chunks.push(value);\n    }\n  } finally { await reader.cancel(); }\n  return Buffer.concat(chunks);\n}\n\nexport async function verifyPublished(sha, fetchImpl = fetch, { verifyProvenance } = {}) {\n  versionFor(sha);\n  const bytes = await download(`${artifactBase}/${sha}/manifest.json`, fetchImpl);","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-migrator-artifacts.mjs#L119-L155","documentation":"download() fetches an immutable artifact over HTTPS with redirects refused and a 60s timeout. This error is thrown when the response status is not ok (2xx); the HTTP status is embedded in both the message and error.cause.status. Callers like publishBundle specifically retry when cause.status is 403/404 (a CDN may have cached a missing-object response just before publication).","triggerScenarios":"verifyPublished (or publishBundle's verifyVisible) calls download() on a manifest.json, package tgz, or lockfile URL and the server/CDN answers 403, 404, 500, etc. — object not yet uploaded, wrong SHA, expired/withheld CDN response, or origin failure.","commonSituations":"Running `verify` for a SHA that was never published; verifying immediately after publish while the CDN still serves a cached 404 within its error TTL; typo'd commit SHA; CloudFront/S3 outage returning 5xx.","solutions":["If cause.status is 403 or 404 right after publishing, wait through the CDN error TTL (~seconds) and retry — publishBundle does this automatically up to 6 attempts at 2s intervals","Confirm the artifact was actually published: check the commit SHA and run `node scripts/cloud-migrator-artifacts.mjs publish <dir> <sha>` if not","Verify the URL/SHA is correct and the S3 object exists (list-objects-v2 on the exact prefix distinguishes missing from permission errors)","For persistent 5xx, check CDN/origin health before retrying"],"exampleFix":"// before (immediate verify can hit cached 404)\nawait verifyPublished(sha); // throws Artifact download failed: HTTP 404\n// after (retry through the CDN error TTL)\nfor (let a = 0; a < 6; a++) {\n  try { return await verifyPublished(sha); }\n  catch (e) { if (![403, 404].includes(e.cause?.status)) throw e; await sleep(2000); }\n}","handlingStrategy":"retry","validationCode":"const head = await fetch(url, { method: \"HEAD\", redirect: \"error\", signal: AbortSignal.timeout(10_000) });\nif (!head.ok) throw new Error(`artifact not available yet: HTTP ${head.status}`);","typeGuard":"const isRetryableArtifactStatus = (status) => status === 403 || status === 404;","tryCatchPattern":"try {\n  const manifest = await verifyPublished(sha);\n} catch (err) {\n  if (err.message.startsWith(\"Artifact download failed\") && [403, 404].includes(err.cause?.status)) {\n    // CDN may hold a cached missing-object response; wait out the error TTL\n    await new Promise((r) => setTimeout(r, 2000));\n  } else throw err;\n}","preventionTips":["Only verify a SHA after publishBundle reports success (the commit marker is uploaded last)","Retry 403/404 for a few seconds after publishing — the CDN caches missing-object responses briefly","Confirm the commit SHA is correct before fetching","Distinguish missing objects from permission errors via an exact-prefix S3 list before concluding the artifact is absent"],"tags":["network","http","cdn","download"],"backgroundTag":"http-error-response","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}