koala73/worldmonitor · warning · ApiError

Service temporarily unavailable

Error message

Service temporarily unavailable

What it means

Same revalidation path as listWebhooks but in registerWebhook: when the gateway reports USER_API_KEY_GATEWAY_VALIDATION_ERROR, the server revalidates the header credential itself via validateUserApiKey. If that call throws (validation backend unavailable), the handler returns HTTP 503 'Service temporarily unavailable' rather than the internal failure.

Solutions

  1. Retry the registration after a short backoff — the 503 indicates a transient server-side condition
  2. Check service health/status before repeated retries
  3. Ensure the API key header is actually attached so validation behaves predictably
  4. If the outage persists, delay webhook registration and re-run your setup script later

Example fix

// before
await client.registerWebhook({ callbackUrl }); // 503 during auth outage
// after
await backoff(() => client.registerWebhook({ callbackUrl }), {
  retries: 3,
  retryOn: (e) => e.status === 503,
});
Defensive patterns

Strategy: retry

Try / catch

try {
  return await client.registerWebhook(req);
} catch (e) {
  if (e.status === 503) {
    await sleep(backoff(attempt++));
    return registerWithRetry(req);
  }
  throw e;
}

Prevention

When it happens

Trigger: POSTing to register-webhook with a user API key during an outage of the key-validation service; validateUserApiKey throws inside the catch and the 503 is raised.

Common situations: Auth backend outage or timeout at peak load; network issue between the API server and the credential store; transient errors right after key rotation while caches propagate.

Related errors


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

Appendix: source

Thrown at server/worldmonitor/shipping/v2/register-webhook.ts:52

): Promise<RegisterWebhookResponse> {
  // Webhooks are per-tenant keyed on callerFingerprint(), which hashes the
  // API key. Without forceKey, a Clerk-authenticated pro caller reaches this
  // handler with no API key, callerFingerprint() falls back to 'anon', and
  // every such caller collapses into a shared 'anon' owner bucket — letting
  // one Clerk-session holder enumerate/overwrite other tenants' webhooks.
  // Matches the legacy `api/v2/shipping/webhooks/[subscriberId]{,/[action]}.ts`
  // gate and the documented "X-WorldMonitor-Key required" contract in
  // docs/api-shipping-v2.mdx.
  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 callbackUrl = (req.callbackUrl ?? '').trim();
  if (!callbackUrl) {
    throw new ValidationError([{ field: 'callbackUrl', description: 'callbackUrl is required' }]);
  }

  try {

View on GitHub (pinned to 7d06c8633d)