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
- 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
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
- Build 503-aware retry with exponential backoff into API clients
- Distinguish 503 (transient) from 401 (credential) before alerting
- Subscribe to service health/status feeds
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
- Service temporarily unavailable
- Revoke service is temporarily unavailable. Try again in a mo
- Authentication unavailable while loading MCP clients. Try ag
- Invalid scorecard bloc selection.
- Railway relay unavailable for relay-only domain: ${hostname}
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)