koala73/worldmonitor · warning · Error

This client was already revoked.

Error message

This client was already revoked.

What it means

Mapped by revokeMcpClient() in src/services/mcp-clients.ts from an HTTP 409 returned by POST /api/user/mcp-revoke. The edge handler forwards Convex's ALREADY_REVOKED outcome: the token row exists and is owned by the caller, but its revokedAt is already set. Classic idempotency conflict — the desired end state (revoked) is already true, typically because an earlier revoke succeeded.

Source

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

  const token = await getClerkToken();
  if (!token) throw new Error('Sign in to revoke MCP clients.');

  const resp = await fetch('/api/user/mcp-revoke', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    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() };

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Treat 409 as success: the token is revoked, so update the UI to the revoked state.
  2. Prevent double-submission (disable the button while the request is in flight).
  3. Refresh listMcpClients() afterwards to confirm the row's state.
  4. Do not surface this as an error to the user — it is the requested outcome already achieved.

Example fix

// before
await revokeMcpClient(tokenId);
markRevoked(tokenId);

// after
try {
  await revokeMcpClient(tokenId);
} catch (err) {
  if (err instanceof Error && err.message === 'This client was already revoked.') {
    // desired state already reached — not an error
  } else {
    throw err;
  }
}
markRevoked(tokenId);
Defensive patterns

Strategy: try-catch

Validate before calling

const clients = await listMcpClients();
const alreadyRevoked = clients.some((c) => c.id === tokenId && c.revokedAt);
if (alreadyRevoked) {
  markRevoked(tokenId);
  return;
}
await revokeMcpClient(tokenId);

Type guard

function isAlreadyRevoked(err: unknown): boolean {
  return err instanceof Error && err.message === 'This client was already revoked.';
}

Try / catch

try {
  await revokeMcpClient(tokenId);
} catch (err) {
  if (isAlreadyRevoked(err)) {
    markRevoked(tokenId); // desired end state already true — success, not failure
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Double-clicking revoke before the first request finishes; two browser tabs revoking the same token; retrying after a first attempt that timed out client-side but actually landed server-side; revoking again after a page that never refreshed its list.

Common situations: Impatient users double-submitting; fire-and-forget retries in UI code; flaky networks where the success response was lost but the mutation committed.

Related errors


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