koala73/worldmonitor · warning
REDIS_DOWN
REDIS_DOWN
Error message
Redis not configured
What it means
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'.
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
Example fix
// before
const keys = await listApiKeys(); // can throw 'Authentication unavailable while loading API keys.'
// after: one bounded retry, then an explicit unavailable state
let keys: ApiKeyInfo[] | null = null;
for (let attempt = 0; attempt < 2 && !keys; attempt++) {
try { keys = await listApiKeys(); }
catch (e) {
if (attempt === 1 || !(e instanceof Error && e.message.includes('Authentication unavailable'))) throw e;
await new Promise(r => setTimeout(r, 1500));
}
}
renderKeys(keys ?? 'unavailable'); Defensive patterns
Strategy: retry
Validate before calling
const userId = getCurrentClerkUser()?.id;
if (!userId) { renderKeys([]); return; } // signed out is genuinely empty
if (!(await getConvexClient())) { renderKeys([]); return; } Type guard
const isAuthUnavailableError = (e: unknown): e is Error =>
e instanceof Error && e.message.startsWith('Authentication unavailable'); Try / catch
try {
const keys = await listApiKeys();
renderKeys(keys);
} catch (e) {
if (isAuthUnavailableError(e)) renderKeys('unavailable'); // NOT empty — truth is unknown
else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Slow networks on first load; ad-blockers blocking Clerk or Convex endpoints; a backgrounded tab with throttled timers; a brief Convex deployment hiccup.
Related errors
- Authentication unavailable while loading Business Pro…
- Authentication unavailable while loading MCP clients. Try…
- HTTP
- Account changed while creating the embed key. Try again.
- ALREADY_REVOKED
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/87240ae15d89706b.
Report an issue: GitHub.
Appendix: source
Thrown at api/health.js:3597
// problem, so adding a new ok-bucket status (NOT_CONFIGURED) can never again
// leave one problem surface silently reporting it. An unregistered status is
// treated as a problem, matching the summary's `STATUS_COUNTS[s] ?? 'warn'`.
function isProblemStatus(status) {
return STATUS_COUNTS[status] !== 'ok';
}
function isPendingHealthEntry(entry, now) {
return isProblemStatus(entry?.status) && healthStatusBucket(entry, now) === 'ok';
}
/**
* True when a cached verdict still carries a health softening whose published
* deadline has passed. Reads both snapshot shapes:
* the full one keyed by `checks`, the compact one by `problems` and `pending`.
*/
function isExpiredDeadline(raw, now) {
const until = Date.parse(typeof raw === 'string' ? raw : '');
// An unparseable deadline is treated as expired: a pending entry that
// cannot prove it is still inside its window must not be served warm.
return !Number.isFinite(until) || now >= until;
}
// Every per-entry softening deadline a snapshot can carry, in one place. Both
// readers below walk this table instead of repeating a `hasOwnProperty` +
// `isExpiredDeadline` block per field, so adding another softening is one
// entry here rather than more near-identical branches that can drift apart.
//
// `kind` decides which `hasExpiredActivationGrace` toggle governs the field.
// `status` pins a field to one status where the field alone is not proof:
// `rolloutPendingUntil` only softens a ROLLOUT_PENDING entry, whereas the two
// content deadlines are emitted precisely when they apply.
const ENTRY_SOFTENING_DEADLINES = [
{ field: 'rolloutPendingUntil', kind: 'rollout', status: 'ROLLOUT_PENDING' },
{ field: 'contentFreshnessPendingUntil', kind: 'content', status: null },
{ field: 'staleContentGraceUntil', kind: 'content', status: null },
{ field: 'sourceFailurePendingUntil', kind: 'source', status: 'SEED_ERROR' },View on GitHub (pinned to e586b8b4b8)