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
- Treat 409 as success: the token is revoked, so update the UI to the revoked state.
- Prevent double-submission (disable the button while the request is in flight).
- Refresh listMcpClients() afterwards to confirm the row's state.
- 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
- Disable the revoke button while a request is in flight to prevent double-submission.
- Optimistically mark the row revoked on submit and reconcile on failure instead of blocking on the response.
- Reconcile the list after every revoke to keep UI and server state aligned.
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
- REPLAY_CONFLICT
- get_intel_timeline requires at least one of domain ("conflic
- ${operation} HTTP ${status}: ${safeCode}
- `call` needs a tool name, e.g. `worldmonitor call get_countr
- COMPANY_MONITORING_CLASSIFICATION_REPLAY_CONFLICT
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/180b36f96334abf2.
Report an issue: GitHub.