paperclipai/paperclip · error · Error
Artifact download failed: HTTP
Error message
Artifact download failed: HTTP ${response.status} What it means
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).
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
Example fix
// before (immediate verify can hit cached 404)
await verifyPublished(sha); // throws Artifact download failed: HTTP 404
// after (retry through the CDN error TTL)
for (let a = 0; a < 6; a++) {
try { return await verifyPublished(sha); }
catch (e) { if (![403, 404].includes(e.cause?.status)) throw e; await sleep(2000); }
} Defensive patterns
Strategy: retry
Validate before calling
const head = await fetch(url, { method: "HEAD", redirect: "error", signal: AbortSignal.timeout(10_000) });
if (!head.ok) throw new Error(`artifact not available yet: HTTP ${head.status}`); Type guard
const isRetryableArtifactStatus = (status) => status === 403 || status === 404;
Try / catch
try {
const manifest = await verifyPublished(sha);
} catch (err) {
if (err.message.startsWith("Artifact download failed") && [403, 404].includes(err.cause?.status)) {
// CDN may hold a cached missing-object response; wait out the error TTL
await new Promise((r) => setTimeout(r, 2000));
} else throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Anthropic Managed Agents request failed with HTTP
- Artifact exceeds size limit.
- `}
- Could not load OpenRouter models. Retry or enter a model ID…
- Discord returned an unreadable response
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/19657254751fcb4b.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/cloud-migrator-artifacts.mjs:137
for (const name of names) {
copyFileSync(path.join(directory, `${name}.tgz`), path.join(scratch, `${name}.tgz`));
// The public objects do not exist yet. Only transport changes for this
// smoke install; exact versions, integrity, root and transitive pins stay.
lock.packages[`node_modules/@paperclipai/${name}`].resolved = `file:${name}.tgz`;
}
writeFileSync(path.join(scratch, "package.json"), JSON.stringify({ name: "paperclip-migrator-install-root", version: "0.0.0", private: true,
dependencies: { "@paperclipai/db": manifest.packageVersion } }));
writeFileSync(path.join(scratch, "package-lock.json"), JSON.stringify(lock));
exec("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund", "--update-notifier=false", "--cache", path.join(scratch, "empty-cache"),
"--registry=https://registry.npmjs.org"], { cwd: scratch, stdio: "inherit", timeout: 180_000 });
for (const name of names) assertMetadata(JSON.parse(readFileSync(path.join(scratch, "node_modules", "@paperclipai", name, "package.json"), "utf8")), `@paperclipai/${name}`, sha);
exec(process.execPath, ["--input-type=module", "--eval", "await import('@paperclipai/db'); await import('@paperclipai/shared');"], { cwd: scratch, stdio: "inherit", timeout: 30_000 });
} finally { rmSync(scratch, { recursive: true, force: true }); }
}
async function download(url, fetchImpl) {
const response = await fetchImpl(url, { redirect: "error", signal: AbortSignal.timeout(60_000) });
if (!response.ok) throw new Error(`Artifact download failed: HTTP ${response.status}`, { cause: { status: response.status } });
const reader = response.body.getReader();
const chunks = [];
let size = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.length;
if (size > maximumBytes) throw new Error("Artifact exceeds size limit.");
chunks.push(value);
}
} finally { await reader.cancel(); }
return Buffer.concat(chunks);
}
export async function verifyPublished(sha, fetchImpl = fetch, { verifyProvenance } = {}) {
versionFor(sha);
const bytes = await download(`${artifactBase}/${sha}/manifest.json`, fetchImpl);View on GitHub (pinned to 3f1d897a7c)