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

  1. Retry the load after a short delay; token propagation usually completes on the next attempt
  2. Verify network access to both Clerk and the Convex deployment (no blocked requests in the Network tab)
  3. Reload the page to force a fresh auth rebind if it persists
  4. 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

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


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)