{"record":{"id":"8058553cd8b45a2e","repo":"paperclipai/paperclip","slug":"public-announcement-asset-headers-are-not-ready","errorCode":null,"errorMessage":"Public announcement asset headers are not ready","messagePattern":"Public announcement asset headers are not ready","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/publish-announcements.ts","lineNumber":116,"sourceCode":"    delete env.AWS_SESSION_TOKEN;\n    env.AWS_PROFILE = env.PAPERCLIP_PAGE_AWS_PROFILE;\n  }\n  // Only validated files, assets before manifest; credentials are scoped to AWS.\n  for (const file of prepared.files) execFileSync(\"aws\", announcementUploadArgs(bucket, file), { env, stdio: \"pipe\" });\n  console.log(\"Uploaded. Checking the public manifest (CDN propagation can take five minutes)…\");\n  for (let attempt = 0; attempt < 23; attempt++) {\n    try {\n      const response = await fetch(url, { signal: AbortSignal.timeout(10_000), credentials: \"omit\", redirect: \"error\" });\n      const body = announcementManifestSchema.parse(await response.json());\n      if (response.ok && JSON.stringify(body) === JSON.stringify(prepared.manifest)\n        && /(?:^|,)\\s*max-age=300(?:\\s*,|$)/i.test(response.headers.get(\"cache-control\") ?? \"\")) {\n        for (const asset of prepared.files.slice(0, -1)) {\n          const image = await fetch(`${baseUrl}/${asset.key}`, { method: \"HEAD\", signal: AbortSignal.timeout(10_000), credentials: \"omit\", redirect: \"error\" });\n          const caching = image.headers.get(\"cache-control\") ?? \"\";\n          if (!image.ok || image.headers.get(\"content-type\") !== asset.contentType\n            || !/(?:^|,)\\s*max-age=31536000(?:\\s*,|$)/i.test(caching)\n            || !/(?:^|,)\\s*immutable(?:\\s*,|$)/i.test(caching)) {\n            throw new Error(\"Public announcement asset headers are not ready\");\n          }\n        }\n        console.log(`Published and verified: ${url}`);\n        return;\n      }\n    } catch { /* Retry edge propagation; uploads have already completed. */ }\n    if (attempt < 22) await new Promise((resolve) => setTimeout(resolve, 15_000));\n  }\n  throw new Error(`Uploaded, but public verification did not finish. Check ${url} and the CloudFront cache policy (minimum TTL must not exceed 300 seconds).`);\n}\n\nif (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {\n  main().catch((error) => {\n    console.error(error instanceof Error ? error.message : \"Announcement publish failed\");\n    process.exitCode = 1;\n  });\n}\n","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/publish-announcements.ts#L98-L134","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the CDN response headers directly: curl -I <baseUrl>/<key> and compare content-type and cache-control with the script's expectations","Set the CloudFront cache policy so it passes through the origin's Cache-Control and its minimum TTL does not exceed 300 seconds","Fix the asset file extension so the computed contentType matches the served one (png/jpg/webp for images, text/html for animations)","Re-run the publish script once edge propagation completes; verification retries handle transient propagation"],"exampleFix":"// before\nCloudFront policy: Cache-Control override, min TTL 31536000\n// after\nCloudFront policy: origin Cache-Control passthrough, min TTL 0 (<= 300s)","handlingStrategy":"retry","validationCode":"const head = await fetch(`${baseUrl}/${key}`, { method: \"HEAD\" });\nconst cc = head.headers.get(\"cache-control\") ?? \"\";\nconsole.assert(head.ok && /max-age=31536000/.test(cc) && /immutable/.test(cc), \"asset headers not ready\");","typeGuard":null,"tryCatchPattern":"try { await verifyAssets(); } catch { await sleep(15_000); /* retry up to ~22 attempts before investigating CDN policy */ }","preventionTips":["Ensure the CloudFront cache policy forwards origin Cache-Control and has minimum TTL <= 300s","Use file extensions the script's contentType mapping expects (png/jpg/webp; html for animations)","Confirm S3 object metadata matches the intended content-type after upload","Verify with curl -I against the public URL before declaring a publish successful"],"tags":["cdn","http","cache","verification"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}