{"record":{"id":"8b2c0659084682aa","repo":"tldraw/tldraw","slug":"failed-to-look-up-zone-cloudflare-preview-zone","errorCode":null,"errorMessage":"Failed to look up zone ${CLOUDFLARE_PREVIEW_ZONE}: ${res.status} ${res.statusText}","messagePattern":"Failed to look up zone (.+?): (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"internal/scripts/prune-preview-deploys.ts","lineNumber":99,"sourceCode":"\t\t\t// Delete workers with service bindings to other workers first (image-optimizer and tldrawusercontent both bind to the sync worker)\n\t\t\t.sort((a, b) => {\n\t\t\t\tconst aHasBinding = a.includes('image-optimizer') || a.includes('tldrawusercontent')\n\t\t\t\tconst bHasBinding = b.includes('image-optimizer') || b.includes('tldrawusercontent')\n\t\t\t\tif (aHasBinding && !bHasBinding) return -1\n\t\t\t\tif (!aHasBinding && bHasBinding) return 1\n\t\t\t\treturn 0\n\t\t\t})\n\t)\n}\n\n// Preview routes and cert packs live on the preview zone rather than the account.\nconst CLOUDFLARE_PREVIEW_ZONE = 'tldraw.xyz'\nlet _previewZoneId: string | undefined\nasync function getPreviewZoneId() {\n\tif (_previewZoneId) return _previewZoneId\n\tconst res = await cloudflareV4Api(`/zones?name=${CLOUDFLARE_PREVIEW_ZONE}`)\n\tif (!res.ok) {\n\t\tthrow new Error(\n\t\t\t`Failed to look up zone ${CLOUDFLARE_PREVIEW_ZONE}: ${res.status} ${res.statusText}`\n\t\t)\n\t}\n\tconst data = (await res.json()) as { success: boolean; result: { id: string }[] }\n\tif (!data.success || !data.result.length) {\n\t\t// an empty result also happens when the token lacks zone-scoped \"Zone: Read\"\n\t\tthrow new Error(`Failed to find zone ${CLOUDFLARE_PREVIEW_ZONE}: ${JSON.stringify(data)}`)\n\t}\n\t_previewZoneId = data.result[0].id\n\treturn _previewZoneId\n}\n\n// Preview workers are reachable via zone routes (\"pr-NNNN-<app>.tldraw.xyz/*\").\n// Deleting a worker does not delete its routes, so prune them separately.\n// Only routes matching this exact preview shape may ever be deleted — anything\n// else on the zone (or anything a future refactor feeds in) must not qualify.\nconst PREVIEW_ROUTE_PATTERN_REGEX = /^pr-\\d+-[a-z0-9-]+\\.tldraw\\.xyz\\/\\*$/\nconst _workerRouteIdCache = new Map<string, string>()","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/tldraw/tldraw/blob/b31086b44731a7d1d9d46be4163ac8ef7417321d/internal/scripts/prune-preview-deploys.ts#L81-L117","documentation":"Thrown by getPreviewZoneId() in the preview cleanup script when the Cloudflare API call to GET /zones?name=tldraw.xyz returns a non-2xx HTTP status. This zone id is the entry point for all zone-scoped cleanup (worker routes, certificate packs), so failure aborts most of the prune job. The result is cached after the first success, so this fires at most once per run.","triggerScenarios":"GET https://api.cloudflare.com/client/v4/zones?name=tldraw.xyz with the CLOUDFLARE_API_TOKEN bearer returns non-OK. Concretely: 401 (token invalid/revoked), 403 (token valid but no zone access), 500/502/503 (Cloudflare incident), or a network failure surfaced as a non-OK Response.","commonSituations":"CLOUDFLARE_API_TOKEN expired or rotated; token minted with account-scoped permissions only, missing zone access; running the script outside GitHub Actions where the secret is injected; Cloudflare API incident during a prune run.","solutions":["Verify the token is alive: curl -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" https://api.cloudflare.com/client/v4/user/tokens/verify","If 403, confirm the token grants at least Zone: Read on tldraw.xyz (or All zones)","If 5xx, check https://www.cloudflarestatus.com and re-run the prune job","Ensure the job has the secret injected — makeEnv fails earlier if CLOUDFLARE_API_TOKEN is unset, so a missing var surfaces before this line"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"async function verifyCloudflareToken(token: string): Promise<boolean> {\n  const res = await fetch('https://api.cloudflare.com/client/v4/user/tokens/verify', {\n    headers: { Authorization: `Bearer ${token}` },\n  })\n  if (!res.ok) return false\n  const data = await res.json() as { success: boolean; result: { status: string } }\n  return data.success && data.result.status === 'active'\n}\n\n// call before main()\nif (!(await verifyCloudflareToken(env.CLOUDFLARE_API_TOKEN))) {\n  throw new Error('CLOUDFLARE_API_TOKEN is invalid or inactive')\n}","typeGuard":null,"tryCatchPattern":"// retry transient 5xx at the API helper layer, fail fast on 4xx\nasync function cloudflareV4ApiWithRetry(endpoint: string, options: RequestInit = {}, retries = 3): Promise<Response> {\n  for (let attempt = 1; ; attempt++) {\n    const res = await cloudflareV4Api(endpoint, options)\n    if (res.ok || res.status < 500 || attempt >= retries) return res\n    await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt))\n  }\n}","preventionTips":["Run the script only in CI where CLOUDFLARE_API_TOKEN is freshly injected","Rotate the token on a schedule and alert before expiry","Verify the token with the tokens/verify endpoint at the start of every run"],"tags":["cloudflare","network","authentication","ci"],"backgroundTag":null,"analyzedSha":"b31086b44731a7d1d9d46be4163ac8ef7417321d","analyzedAt":"2026-08-12T17:05:39.947Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}