{"record":{"id":"87240ae15d89706b","repo":"koala73/worldmonitor","slug":"redis-down","errorCode":"REDIS_DOWN","errorMessage":"Redis not configured","messagePattern":"Redis not configured","errorType":"http","errorClass":null,"httpStatus":503,"severity":"warning","filePath":"api/health.js","lineNumber":3006,"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":2988,"sourceCodeEnd":3024,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/api/health.js#L2988-L3024","documentation":"listApiKeys() treats a null Clerk user or null Convex client as an empty list (no throw). It throws only when waitForConvexAuthForUser(userId) is false while assertAccountStillCurrent(userId) passes, i.e., the user is still signed in as the same person but the Convex auth token never became ready within the 10s barrier timeout. This distinguishes a transient token-propagation failure from a real account switch, so the UI can show 'unknown' instead of a false 'no keys'.","triggerScenarios":"Clerk session valid but the Convex auth rebind (startConvexAuthRebind/installConvexAuth) never delivers a server-confirmed token within 10s; slow or blocked token fetch; WebSocket pause preventing the barrier from completing.","commonSituations":"Slow networks on first load; ad-blockers blocking Clerk or Convex endpoints; a backgrounded tab with throttled timers; a brief Convex deployment hiccup.","solutions":["Retry the load after a short delay; token propagation usually completes on the next attempt","Verify network access to both Clerk and the Convex deployment (no blocked requests in the Network tab)","Reload the page to force a fresh auth rebind if it persists","Render the key list as 'unavailable, retry' rather than empty when this error fires"],"exampleFix":"// before\nconst keys = await listApiKeys(); // can throw 'Authentication unavailable while loading API keys.'\n\n// after: one bounded retry, then an explicit unavailable state\nlet keys: ApiKeyInfo[] | null = null;\nfor (let attempt = 0; attempt < 2 && !keys; attempt++) {\n  try { keys = await listApiKeys(); }\n  catch (e) {\n    if (attempt === 1 || !(e instanceof Error && e.message.includes('Authentication unavailable'))) throw e;\n    await new Promise(r => setTimeout(r, 1500));\n  }\n}\nrenderKeys(keys ?? 'unavailable');","handlingStrategy":"retry","validationCode":"const userId = getCurrentClerkUser()?.id;\nif (!userId) { renderKeys([]); return; } // signed out is genuinely empty\nif (!(await getConvexClient())) { renderKeys([]); return; }","typeGuard":"const isAuthUnavailableError = (e: unknown): e is Error =>\n  e instanceof Error && e.message.startsWith('Authentication unavailable');","tryCatchPattern":"try {\n  const keys = await listApiKeys();\n  renderKeys(keys);\n} catch (e) {\n  if (isAuthUnavailableError(e)) renderKeys('unavailable'); // NOT empty — truth is unknown\n  else throw e;\n}","preventionTips":["Never render this failure as an empty list; show an explicit 'unavailable, retry' state","One bounded retry after 1-2s covers most slow token propagation","Check for blocked Clerk/Convex requests when it recurs"],"tags":["clerk","convex","auth-token","timeout","api-keys"],"backgroundTag":"auth-token-not-ready","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T11:17:13.642Z"}