{"record":{"id":"180b36f96334abf2","repo":"koala73/worldmonitor","slug":"this-client-was-already-revoked","errorCode":null,"errorMessage":"This client was already revoked.","messagePattern":"This client was already revoked\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/services/mcp-clients.ts","lineNumber":94,"sourceCode":"  const token = await getClerkToken();\n  if (!token) throw new Error('Sign in to revoke MCP clients.');\n\n  const resp = await fetch('/api/user/mcp-revoke', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${token}`,\n    },\n    body: JSON.stringify({ tokenId }),\n  });\n\n  if (resp.ok) return;\n\n  if (resp.status === 404) {\n    throw new Error('This client was already revoked or no longer exists.');\n  }\n  if (resp.status === 409) {\n    throw new Error('This client was already revoked.');\n  }\n  if (resp.status === 401) {\n    throw new Error('Sign in to revoke MCP clients.');\n  }\n  if (resp.status === 503) {\n    throw new Error('Revoke service is temporarily unavailable. Try again in a moment.');\n  }\n  throw new Error(`Revoke failed (HTTP ${resp.status}).`);\n}\n\n/**\n * Fetch the caller's daily Pro MCP quota usage. Returns sane defaults on\n * any failure — the settings UI is informational and should never break\n * because the quota counter is unreachable.\n */\nexport async function fetchMcpQuota(): Promise<McpQuota> {\n  const fallback: McpQuota = { used: 0, limit: 50, resetsAt: nextUtcMidnightIso() };\n","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/services/mcp-clients.ts#L76-L112","documentation":"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.","triggerScenarios":"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.","commonSituations":"Impatient users double-submitting; fire-and-forget retries in UI code; flaky networks where the success response was lost but the mutation committed.","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."],"exampleFix":"// before\nawait revokeMcpClient(tokenId);\nmarkRevoked(tokenId);\n\n// after\ntry {\n  await revokeMcpClient(tokenId);\n} catch (err) {\n  if (err instanceof Error && err.message === 'This client was already revoked.') {\n    // desired state already reached — not an error\n  } else {\n    throw err;\n  }\n}\nmarkRevoked(tokenId);","handlingStrategy":"try-catch","validationCode":"const clients = await listMcpClients();\nconst alreadyRevoked = clients.some((c) => c.id === tokenId && c.revokedAt);\nif (alreadyRevoked) {\n  markRevoked(tokenId);\n  return;\n}\nawait revokeMcpClient(tokenId);","typeGuard":"function isAlreadyRevoked(err: unknown): boolean {\n  return err instanceof Error && err.message === 'This client was already revoked.';\n}","tryCatchPattern":"try {\n  await revokeMcpClient(tokenId);\n} catch (err) {\n  if (isAlreadyRevoked(err)) {\n    markRevoked(tokenId); // desired end state already true — success, not failure\n    return;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["http-409","mcp","idempotency","conflict","double-submit"],"backgroundTag":null,"analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}