tldraw/tldraw · error · Error
Failed to look up zone ${CLOUDFLARE_PREVIEW_ZONE}: ${res.sta
Error message
Failed to look up zone ${CLOUDFLARE_PREVIEW_ZONE}: ${res.status} ${res.statusText} What it means
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.
Source
Thrown at internal/scripts/prune-preview-deploys.ts:99
// Delete workers with service bindings to other workers first (image-optimizer and tldrawusercontent both bind to the sync worker)
.sort((a, b) => {
const aHasBinding = a.includes('image-optimizer') || a.includes('tldrawusercontent')
const bHasBinding = b.includes('image-optimizer') || b.includes('tldrawusercontent')
if (aHasBinding && !bHasBinding) return -1
if (!aHasBinding && bHasBinding) return 1
return 0
})
)
}
// Preview routes and cert packs live on the preview zone rather than the account.
const CLOUDFLARE_PREVIEW_ZONE = 'tldraw.xyz'
let _previewZoneId: string | undefined
async function getPreviewZoneId() {
if (_previewZoneId) return _previewZoneId
const res = await cloudflareV4Api(`/zones?name=${CLOUDFLARE_PREVIEW_ZONE}`)
if (!res.ok) {
throw new Error(
`Failed to look up zone ${CLOUDFLARE_PREVIEW_ZONE}: ${res.status} ${res.statusText}`
)
}
const data = (await res.json()) as { success: boolean; result: { id: string }[] }
if (!data.success || !data.result.length) {
// an empty result also happens when the token lacks zone-scoped "Zone: Read"
throw new Error(`Failed to find zone ${CLOUDFLARE_PREVIEW_ZONE}: ${JSON.stringify(data)}`)
}
_previewZoneId = data.result[0].id
return _previewZoneId
}
// Preview workers are reachable via zone routes ("pr-NNNN-<app>.tldraw.xyz/*").
// Deleting a worker does not delete its routes, so prune them separately.
// Only routes matching this exact preview shape may ever be deleted — anything
// else on the zone (or anything a future refactor feeds in) must not qualify.
const PREVIEW_ROUTE_PATTERN_REGEX = /^pr-\d+-[a-z0-9-]+\.tldraw\.xyz\/\*$/
const _workerRouteIdCache = new Map<string, string>()View on GitHub (pinned to b31086b447)
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
Defensive patterns
Strategy: retry
Validate before calling
async function verifyCloudflareToken(token: string): Promise<boolean> {
const res = await fetch('https://api.cloudflare.com/client/v4/user/tokens/verify', {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) return false
const data = await res.json() as { success: boolean; result: { status: string } }
return data.success && data.result.status === 'active'
}
// call before main()
if (!(await verifyCloudflareToken(env.CLOUDFLARE_API_TOKEN))) {
throw new Error('CLOUDFLARE_API_TOKEN is invalid or inactive')
} Try / catch
// retry transient 5xx at the API helper layer, fail fast on 4xx
async function cloudflareV4ApiWithRetry(endpoint: string, options: RequestInit = {}, retries = 3): Promise<Response> {
for (let attempt = 1; ; attempt++) {
const res = await cloudflareV4Api(endpoint, options)
if (res.ok || res.status < 500 || attempt >= retries) return res
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt))
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to list workers ${JSON.stringify(data)}
- Analytics Engine query failed (${response.status}): ${await
- Could not find the deploy ID in wrangler output
- Could not find the worker name in wrangler output
- Failed to fetch ${url}: ${res.status}
AI-assisted analysis of tldraw/tldraw@b31086b447 (2026-08-12).
Data as JSON: /api/errors/8b2c0659084682aa.
Report an issue: GitHub.