{"record":{"id":"c308448b6aa604fe","repo":"koala73/worldmonitor","slug":"redis-not-configured","errorCode":null,"errorMessage":"Redis not configured","messagePattern":"Redis not configured","errorType":"exception","errorClass":"Error","httpStatus":503,"severity":"critical","filePath":"api/health.js","lineNumber":2624,"sourceCode":"    const lastFailureRaw = results?.[0]?.result;\n    const failureLogRaw = results?.[1]?.result;\n    const body = {\n      lastFailure: parseJson(lastFailureRaw),\n      failureLog: Array.isArray(failureLogRaw)\n        ? failureLogRaw.map(parseJson).filter((e) => e !== null)\n        : [],\n      checkedAt: new Date().toISOString(),\n    };\n    return new Response(JSON.stringify(body, null, 2), { status: 200, headers });\n  }\n\n  // A snapshot hit is one Redis command instead of the ~390-command registry\n  // sweep below. A failed snapshot read is a real Redis outage, not a cache\n  // miss: returning 503 preserves UptimeRobot's hard-down signal.\n  let refreshLockToken = null;\n  let ownsSnapshotRefreshLock = false;\n  try {\n    if (!getRedisCredentials()) throw new Error('Redis not configured');\n    // Read the snapshot this request will actually render. `?compact=1` — the\n    // browser poll, ~115k/day — reads the ~1 KB compact key instead of dragging the\n    // full ~20 KB check map out of Redis to show a tenth of it (#5300).\n    const snapshotKey = compact ? HEALTH_VERDICT_COMPACT_SNAPSHOT_KEY : HEALTH_VERDICT_SNAPSHOT_KEY;\n    const snapshotResult = await redisPipeline([['GET', snapshotKey]], 4_000);\n    if (!snapshotResult) throw new Error('Redis request failed');\n    if (snapshotResult[0]?.error) throw new Error('Redis snapshot read failed');\n    const cachedSnapshot = parseHealthVerdictSnapshot(snapshotResult[0]?.result, snapshotNow(), { requireChecks: !compact });\n    // Activation deadlines are exact to the second, so the 60s verdict cache\n    // must not outlive either rollout grace. A snapshot written just before a\n    // deadline would otherwise keep serving a softened verdict for up to a\n    // minute after strictness was supposed to begin. Sweep fresh instead.\n    if (cachedSnapshot && !hasExpiredActivationGrace(cachedSnapshot, snapshotNow())) {\n      return healthResponse(cachedSnapshot, compact, headers);\n    }\n\n    refreshLockToken = `${now}:${crypto.randomUUID()}`;\n    let lockResult = await redisPipeline([[","sourceCodeStart":2606,"sourceCodeEnd":2642,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/health.js#L2606-L2642","documentation":"Thrown at the top of the health snapshot read path when getRedisCredentials() returns null, meaning at least one of UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is unset. The health endpoint treats Redis as a hard dependency (it can assess nothing without it), so this surfaces as a 503 REDIS_DOWN response that UptimeRobot/k8s probes read as a hard failure. It fires before any Redis command is attempted.","triggerScenarios":"A GET /api/health (or ?compact=1) request hits api/health.js:2624 on a Vercel Edge deployment where the Upstash Redis env vars were never set, were renamed, or were stripped from the preview/branch environment. Also triggered locally without a .env.local carrying the Upstash credentials.","commonSituations":"New preview/branch deploys that did not inherit the production Upstash env vars; a renamed or rotated env var after a secret rotation; running the endpoint in a fresh worktree that was not bootstrapped with `npm run worktree:env`; misconfigured Railway/Docker relay missing the same vars.","solutions":["Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in the Vercel project environment (or .env.local for local dev) and redeploy.","Run `npm run worktree:env` (or worktree:bootstrap) in a fresh worktree to link the ignored .env.local that carries the Upstash credentials.","Verify with `vercel env ls` (or the Vercel dashboard) that both keys exist for the target environment (Production/Preview/Development).","Confirm the credentials point at a reachable Upstash REST endpoint by issuing one pipeline GET out-of-band."],"exampleFix":"// before (env missing)\n// UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN unset\n//\n// after (in .env.local / Vercel project env)\nUPSTASH_REDIS_REST_URL=https://your-db.upstash.io\nUPSTASH_REDIS_REST_TOKEN=AX...token","handlingStrategy":"validation","validationCode":"import { getRedisCredentials } from './_upstash-json.js';\n\nfunction assertRedisConfigured() {\n  if (!getRedisCredentials()) {\n    throw new Error('Missing UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN');\n  }\n}\n// Call at the start of any code path that depends on Redis.","typeGuard":"function redisConfigured(): boolean {\n  return Boolean(process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN);\n}","tryCatchPattern":"try {\n  if (!getRedisCredentials()) throw new Error('Redis not configured');\n  // ... snapshot read\n} catch (err) {\n  // health.js already maps this to a 503 REDIS_DOWN; callers of redisPipeline\n  // should distinguish missing-config (env fix) from outage (retry).\n  if (err.message === 'Redis not configured') {\n    // surface as a deployment-config incident, not a transient retry\n  }\n}","preventionTips":["Add a startup self-check in non-production environments that asserts UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are set.","Keep the Upstash env vars in the Vercel project (all environments: Production, Preview, Development) and verify with `vercel env ls` after rotations.","Run `npm run worktree:env` in fresh worktrees so the ignored .env.local is linked.","Treat 'Redis not configured' as a config error distinct from runtime outage — do not retry, fix the env."],"tags":["redis","health","configuration","edge-function","env-vars"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}