{"record":{"id":"a14a905230993142","repo":"koala73/worldmonitor","slug":"revoke-failed-http-resp-status","errorCode":null,"errorMessage":"Revoke failed (HTTP ${resp.status}).","messagePattern":"Revoke failed \\(HTTP (.+?)\\)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/mcp-clients.ts","lineNumber":102,"sourceCode":"    },\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\n  const token = await getClerkToken();\n  if (!token) return fallback;\n\n  try {\n    const resp = await fetch('/api/user/mcp-quota', {\n      method: 'GET',\n      headers: { Authorization: `Bearer ${token}` },\n    });","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/services/mcp-clients.ts#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Parse the HTTP status out of the message and branch on it — only 503-style transient causes deserve a retry.","For 400: validate tokenId is a non-empty string before calling, and match it against the current listMcpClients() rows.","For 429: back off and retry with exponential delay; audit what is issuing bursts of revokes.","For anything else, capture the response body (add temporary logging in the fetch wrapper) and inspect the edge function logs for the underlying error."],"exampleFix":"// before\nawait revokeMcpClient(tokenId);\n\n// after\nif (typeof tokenId !== 'string' || tokenId.length === 0) {\n  throw new Error('Cannot revoke: no client selected.');\n}\ntry {\n  await revokeMcpClient(tokenId);\n} catch (err) {\n  const m = err instanceof Error ? err.message.match(/HTTP (\\d+)/) : null;\n  if (m && m[1] === '429') { /* schedule retry */ } else { throw err; }\n}","handlingStrategy":"try-catch","validationCode":"if (typeof tokenId !== 'string' || tokenId.trim().length === 0) {\n  throw new Error('Cannot revoke: no client selected.');\n}\nawait revokeMcpClient(tokenId);","typeGuard":"function revokeHttpStatus(err: unknown): number | null {\n  const m = err instanceof Error ? err.message.match(/^Revoke failed \\(HTTP (\\d+)\\)\\.$/) : null;\n  return m ? Number(m[1]) : null;\n}","tryCatchPattern":"try {\n  await revokeMcpClient(tokenId);\n} catch (err) {\n  const status = revokeHttpStatus(err);\n  if (status === 429) { scheduleBackoffRetry(tokenId); return; }\n  if (status === 400) { fixCallerPayload(); return; }\n  throw err; // unknown — surface with status captured for triage\n}","preventionTips":["Always inspect the embedded status before deciding to retry — only transient codes (429/5xx) merit it.","Validate tokenId as a non-empty string drawn from a fresh listMcpClients() result.","Log the response body for unexpected statuses (extend the fetch wrapper) to speed triage."],"tags":["http-status","mcp","catch-all","rate-limit","edge-functions"],"backgroundTag":"unexpected-http-status","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}