paperclipai/paperclip · error · Error

Uploaded, but public verification did not finish. Check

Error message

Uploaded, but public verification did not finish. Check ${url} and the CloudFront cache policy (minimum TTL must not exceed 300 seconds).

What it means

After uploads, the script retries public verification (HEAD checks on assets and current.json) up to 22 attempts 15 seconds apart. If no attempt succeeds, uploads are already done but the public URLs never verified, so this final error is thrown pointing at the URL and the CloudFront cache policy (minimum TTL must not exceed 300s).

Solutions

  1. Confirm the CloudFront cache policy minimum TTL is <= 300 seconds so current.json (max-age=300) expires promptly
  2. Manually create a CloudFront invalidation for the publish prefix and re-run verification or the script
  3. Check curl -I <url> and the asset HEAD responses to see whether failures are 404 (propagation/origin) vs wrong headers (policy)
  4. Verify PAPERCLIP_PAGE_BASE_URL and PAPERCLIP_PAGE_BUCKET point at the host/bucket actually backed by the CDN distribution

Example fix

// before
CachePolicy: minTTL=3600 on current.json
// after
CachePolicy: minTTL=0, defaultTTL=300 for current.json; rerun publish script
Defensive patterns

Strategy: retry

Validate before calling

const head = await fetch(url, { method: "HEAD" });
if (!head.ok) console.warn(`current.json not yet verifiable: ${head.status}`);

Try / catch

try { await main(); } catch (e) { if (e.message.includes("public verification did not finish")) { /* uploads done: check CDN TTL/policy, invalidate cache, re-verify */ } }

Prevention

When it happens

Trigger: All ~5.5 minutes of verification attempts fail because the CDN never serves the new objects with correct headers/content-type, or current.json never reflects the new manifest within its max-age=300 window.

Common situations: CloudFront minimum TTL set above 300 seconds pinning stale current.json; a distribution misconfiguration serving 403/404 for the prefix; wrong PAPERCLIP_PAGE_BASE_URL so verification hits a different host than was uploaded to; S3 upload to a different bucket than the CDN origin.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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

Appendix: source

Thrown at scripts/publish-announcements.ts:125

      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)