koala73/worldmonitor · warning · ApiError

Service temporarily unavailable

Error message

Service temporarily unavailable

What it means

When the gateway-level API key check reports USER_API_KEY_GATEWAY_VALIDATION_ERROR, listWebhooks revalidates the caller's key itself by calling validateUserApiKey. If that revalidation throws (key service unreachable/erroring), the handler deliberately maps the failure to HTTP 503 'Service temporarily unavailable' instead of leaking the internal error.

Solutions

  1. Retry the request with exponential backoff — 503 here signals a transient backend issue, not a bad key
  2. Check the service status page / health endpoints for an authentication-backend outage
  3. Confirm your API key is present in the header (a missing header means credential is null and this path is skipped differently)
  4. If it persists, report the outage; the client cannot fix a server-side validation failure

Example fix

// before
const hooks = await client.listWebhooks(); // throws 503 during outage
// after
async function listWebhooksWithRetry() {
  for (let attempt = 0; attempt < 3; attempt++) {
    try { return await client.listWebhooks(); }
    catch (e) {
      if (e.status !== 503 || attempt === 2) throw e;
      await new Promise(r => setTimeout(r, 2 ** attempt * 500));
    }
  }
}
Defensive patterns

Strategy: retry

Try / catch

try {
  return await client.listWebhooks();
} catch (e) {
  if (e.status === 503 && e.message === 'Service temporarily unavailable') {
    await sleep(backoff(attempt++));
    return listWebhooksWithRetry();
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending a user API key header to list-webhooks while the user-key validation backend is down, timing out, or returning an error; validateUserApiKey rejects inside the catch block.

Common situations: Upstream auth service outage or rate limiting; network partition between edge/server and the key store; expired rotating credentials that now fail validation exceptionally rather than returning null.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/b33e0b9339e4213c. Report an issue: GitHub.

Appendix: source

Thrown at server/worldmonitor/shipping/v2/list-webhooks.ts:39

export async function listWebhooks(
  ctx: ServerContext,
  _req: ListWebhooksRequest,
): Promise<ListWebhooksResponse> {
  // Without forceKey, Clerk-authenticated pro callers reach this handler with
  // no API key, callerFingerprint() returns the 'anon' fallback, and the
  // ownerTag !== ownerHash defense-in-depth below collapses because both
  // sides equal 'anon' — exposing every 'anon'-bucket tenant's webhooks to
  // every Clerk-session holder. See registerWebhook for full rationale.
  const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {
    valid: boolean; required: boolean; error?: string; credential?: string;
  };
  if (apiKeyResult.error === USER_API_KEY_GATEWAY_VALIDATION_ERROR) {
    const credential = getHeaderApiKey(ctx.request) as string;
    let userKey;
    try {
      userKey = credential ? await validateUserApiKey(credential) : null;
    } catch {
      throw new ApiError(503, 'Service temporarily unavailable', '');
    }
    if (!userKey) throw new ApiError(401, 'Invalid API key', '');
    // Revalidate the credential rather than trusting a caller-supplied user ID.
    apiKeyResult.valid = true;
    apiKeyResult.credential = credential;
  }
  if (apiKeyResult.required && !apiKeyResult.valid) {
    throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');
  }

  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');

  const ownerHash = await callerFingerprint(ctx.request, apiKeyResult.credential);
  const records = await readOwnerWebhooks(ownerHash);
  const webhooks: WebhookSummary[] = [];
  for (const value of records) {
    try {
      const record = JSON.parse(value) as WebhookRecord;

View on GitHub (pinned to 7d06c8633d)