paperclipai/paperclip · error · Error

Public announcement asset headers are not ready

Error message

Public announcement asset headers are not ready

What it means

After uploading, the script verifies each asset at the public CDN with a HEAD request and requires HTTP 200, the exact expected Content-Type, and Cache-Control containing both max-age=31536000 and immutable. This error means at least one uploaded asset failed those header checks during a verification attempt (retried for edge propagation).

Solutions

  1. Check the CDN response headers directly: curl -I <baseUrl>/<key> and compare content-type and cache-control with the script's expectations
  2. Set the CloudFront cache policy so it passes through the origin's Cache-Control and its minimum TTL does not exceed 300 seconds
  3. Fix the asset file extension so the computed contentType matches the served one (png/jpg/webp for images, text/html for animations)
  4. Re-run the publish script once edge propagation completes; verification retries handle transient propagation

Example fix

// before
CloudFront policy: Cache-Control override, min TTL 31536000
// after
CloudFront policy: origin Cache-Control passthrough, min TTL 0 (<= 300s)
Defensive patterns

Strategy: retry

Validate before calling

const head = await fetch(`${baseUrl}/${key}`, { method: "HEAD" });
const cc = head.headers.get("cache-control") ?? "";
console.assert(head.ok && /max-age=31536000/.test(cc) && /immutable/.test(cc), "asset headers not ready");

Try / catch

try { await verifyAssets(); } catch { await sleep(15_000); /* retry up to ~22 attempts before investigating CDN policy */ }

Prevention

When it happens

Trigger: CloudFront/S3 serving a different content-type than computed for the asset; CDN cache policy overriding or stripping Cache-Control so max-age=31536000/immutable is absent; an asset not yet propagated or 404 at the edge during all attempts.

Common situations: CloudFront distribution whose default TTL or response-header policy rewrites Cache-Control; S3 metadata for the object differing from the script's contentType mapping (e.g. wrong extension -> image/webp fallback); CDN not yet serving the freshly uploaded keys.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/8058553cd8b45a2e. Report an issue: GitHub.

Appendix: source

Thrown at scripts/publish-announcements.ts:116

    delete env.AWS_SESSION_TOKEN;
    env.AWS_PROFILE = env.PAPERCLIP_PAGE_AWS_PROFILE;
  }
  // Only validated files, assets before manifest; credentials are scoped to AWS.
  for (const file of prepared.files) execFileSync("aws", announcementUploadArgs(bucket, file), { env, stdio: "pipe" });
  console.log("Uploaded. Checking the public manifest (CDN propagation can take five minutes)…");
  for (let attempt = 0; attempt < 23; attempt++) {
    try {
      const response = await fetch(url, { signal: AbortSignal.timeout(10_000), credentials: "omit", redirect: "error" });
      const body = announcementManifestSchema.parse(await response.json());
      if (response.ok && JSON.stringify(body) === JSON.stringify(prepared.manifest)
        && /(?:^|,)\s*max-age=300(?:\s*,|$)/i.test(response.headers.get("cache-control") ?? "")) {
        for (const asset of prepared.files.slice(0, -1)) {
          const image = await fetch(`${baseUrl}/${asset.key}`, { method: "HEAD", signal: AbortSignal.timeout(10_000), credentials: "omit", redirect: "error" });
          const caching = image.headers.get("cache-control") ?? "";
          if (!image.ok || image.headers.get("content-type") !== asset.contentType
            || !/(?:^|,)\s*max-age=31536000(?:\s*,|$)/i.test(caching)
            || !/(?:^|,)\s*immutable(?:\s*,|$)/i.test(caching)) {
            throw new Error("Public announcement asset headers are not ready");
          }
        }
        console.log(`Published and verified: ${url}`);
        return;
      }
    } catch { /* Retry edge propagation; uploads have already completed. */ }
    if (attempt < 22) await new Promise((resolve) => setTimeout(resolve, 15_000));
  }
  throw new Error(`Uploaded, but public verification did not finish. Check ${url} and the CloudFront cache policy (minimum TTL must not exceed 300 seconds).`);
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main().catch((error) => {
    console.error(error instanceof Error ? error.message : "Announcement publish failed");
    process.exitCode = 1;
  });
}

View on GitHub (pinned to 3f1d897a7c)