koala73/worldmonitor · error · Error

Revoke failed (HTTP ${resp.status}).

Error message

Revoke failed (HTTP ${resp.status}).

What it means

Catch-all thrown by revokeMcpClient() in src/services/mcp-clients.ts for any response status outside {200, 404, 409, 401, 503}. The status is embedded in the message, so the number identifies the branch: 400 means missing/empty tokenId or invalid JSON body, 405 means a non-POST reached the route, 429 is edge rate limiting, and other 5xx are platform-level failures of the /api/user/mcp-revoke handler itself rather than the typed Convex outcomes.

Source

Thrown at src/services/mcp-clients.ts:102

    },
    body: JSON.stringify({ tokenId }),
  });

  if (resp.ok) return;

  if (resp.status === 404) {
    throw new Error('This client was already revoked or no longer exists.');
  }
  if (resp.status === 409) {
    throw new Error('This client was already revoked.');
  }
  if (resp.status === 401) {
    throw new Error('Sign in to revoke MCP clients.');
  }
  if (resp.status === 503) {
    throw new Error('Revoke service is temporarily unavailable. Try again in a moment.');
  }
  throw new Error(`Revoke failed (HTTP ${resp.status}).`);
}

/**
 * Fetch the caller's daily Pro MCP quota usage. Returns sane defaults on
 * any failure — the settings UI is informational and should never break
 * because the quota counter is unreachable.
 */
export async function fetchMcpQuota(): Promise<McpQuota> {
  const fallback: McpQuota = { used: 0, limit: 50, resetsAt: nextUtcMidnightIso() };

  const token = await getClerkToken();
  if (!token) return fallback;

  try {
    const resp = await fetch('/api/user/mcp-quota', {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
    });

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Parse the HTTP status out of the message and branch on it — only 503-style transient causes deserve a retry.
  2. For 400: validate tokenId is a non-empty string before calling, and match it against the current listMcpClients() rows.
  3. For 429: back off and retry with exponential delay; audit what is issuing bursts of revokes.
  4. For anything else, capture the response body (add temporary logging in the fetch wrapper) and inspect the edge function logs for the underlying error.

Example fix

// before
await revokeMcpClient(tokenId);

// after
if (typeof tokenId !== 'string' || tokenId.length === 0) {
  throw new Error('Cannot revoke: no client selected.');
}
try {
  await revokeMcpClient(tokenId);
} catch (err) {
  const m = err instanceof Error ? err.message.match(/HTTP (\d+)/) : null;
  if (m && m[1] === '429') { /* schedule retry */ } else { throw err; }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (typeof tokenId !== 'string' || tokenId.trim().length === 0) {
  throw new Error('Cannot revoke: no client selected.');
}
await revokeMcpClient(tokenId);

Type guard

function revokeHttpStatus(err: unknown): number | null {
  const m = err instanceof Error ? err.message.match(/^Revoke failed \(HTTP (\d+)\)\.$/) : null;
  return m ? Number(m[1]) : null;
}

Try / catch

try {
  await revokeMcpClient(tokenId);
} catch (err) {
  const status = revokeHttpStatus(err);
  if (status === 429) { scheduleBackoffRetry(tokenId); return; }
  if (status === 400) { fixCallerPayload(); return; }
  throw err; // unknown — surface with status captured for triage
}

Prevention

When it happens

Trigger: Sending an empty or non-string tokenId (400 missing_token_id); a malformed JSON body (400 invalid_json); calling the endpoint with GET (405); Cloudflare/Vercel rate limiting (429); an unhandled exception in the edge handler producing a plain 500.

Common situations: UI bugs passing undefined tokenIds after list refreshes reordered rows; prefetchers/scanners hitting the route with GET; bursts of revoke clicks tripping rate limits; regressions in the edge handler code.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/a14a905230993142. Report an issue: GitHub.