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

  1. 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
  2. Confirm the artifact was actually published: check the commit SHA and run `node scripts/cloud-migrator-artifacts.mjs publish <dir> <sha>` if not
  3. Verify the URL/SHA is correct and the S3 object exists (list-objects-v2 on the exact prefix distinguishes missing from permission errors)
  4. 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

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


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)