{"record":{"id":"b33e0b9339e4213c","repo":"koala73/worldmonitor","slug":"service-temporarily-unavailable","errorCode":null,"errorMessage":"Service temporarily unavailable","messagePattern":"Service temporarily unavailable","errorType":"http","errorClass":"ApiError","httpStatus":503,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/list-webhooks.ts","lineNumber":39,"sourceCode":"export async function listWebhooks(\n  ctx: ServerContext,\n  _req: ListWebhooksRequest,\n): Promise<ListWebhooksResponse> {\n  // Without forceKey, Clerk-authenticated pro callers reach this handler with\n  // no API key, callerFingerprint() returns the 'anon' fallback, and the\n  // ownerTag !== ownerHash defense-in-depth below collapses because both\n  // sides equal 'anon' — exposing every 'anon'-bucket tenant's webhooks to\n  // every Clerk-session holder. See registerWebhook for full rationale.\n  const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {\n    valid: boolean; required: boolean; error?: string; credential?: string;\n  };\n  if (apiKeyResult.error === USER_API_KEY_GATEWAY_VALIDATION_ERROR) {\n    const credential = getHeaderApiKey(ctx.request) as string;\n    let userKey;\n    try {\n      userKey = credential ? await validateUserApiKey(credential) : null;\n    } catch {\n      throw new ApiError(503, 'Service temporarily unavailable', '');\n    }\n    if (!userKey) throw new ApiError(401, 'Invalid API key', '');\n    // Revalidate the credential rather than trusting a caller-supplied user ID.\n    apiKeyResult.valid = true;\n    apiKeyResult.credential = credential;\n  }\n  if (apiKeyResult.required && !apiKeyResult.valid) {\n    throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');\n  }\n\n  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');\n\n  const ownerHash = await callerFingerprint(ctx.request, apiKeyResult.credential);\n  const records = await readOwnerWebhooks(ownerHash);\n  const webhooks: WebhookSummary[] = [];\n  for (const value of records) {\n    try {\n      const record = JSON.parse(value) as WebhookRecord;","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/shipping/v2/list-webhooks.ts#L21-L57","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the request with exponential backoff — 503 here signals a transient backend issue, not a bad key","Check the service status page / health endpoints for an authentication-backend outage","Confirm your API key is present in the header (a missing header means credential is null and this path is skipped differently)","If it persists, report the outage; the client cannot fix a server-side validation failure"],"exampleFix":"// before\nconst hooks = await client.listWebhooks(); // throws 503 during outage\n// after\nasync function listWebhooksWithRetry() {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    try { return await client.listWebhooks(); }\n    catch (e) {\n      if (e.status !== 503 || attempt === 2) throw e;\n      await new Promise(r => setTimeout(r, 2 ** attempt * 500));\n    }\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await client.listWebhooks();\n} catch (e) {\n  if (e.status === 503 && e.message === 'Service temporarily unavailable') {\n    await sleep(backoff(attempt++));\n    return listWebhooksWithRetry();\n  }\n  throw e;\n}","preventionTips":["Build 503-aware retry with exponential backoff into API clients","Distinguish 503 (transient) from 401 (credential) before alerting","Subscribe to service health/status feeds"],"tags":["api","http-503","availability","retryable"],"backgroundTag":"upstream-api-error","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}