{"record":{"id":"a08009c815b713e9","repo":"odysseus-dev/odysseus","slug":"revoke-failed","errorCode":null,"errorMessage":"Revoke failed","messagePattern":"Revoke failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/settings.js","lineNumber":5388,"sourceCode":"        setTimeout(() => { formEl.style.display = 'none'; }, 350);\n      } catch (err) {\n        if (msg) { msg.textContent = err?.message || 'Save failed'; msg.style.color = 'var(--red)'; }\n      }\n    });\n\n    // Revoke = delete this agent token entirely. Confirmation prompt keeps\n    // it from being a one-click footgun. Closes the form on success.\n    el('uf-codex-revoke')?.addEventListener('click', async () => {\n      const tokenId = formEl.dataset.createdTokenId;\n      if (!tokenId) return;\n      const ok = window.styledConfirm\n        ? await window.styledConfirm(`Revoke this ${cfg.word} agent token? Integrations using it will lose access.`, { confirmText: 'Revoke', danger: true })\n        : confirm(`Revoke this ${cfg.word} agent token? Integrations using it will lose access.`);\n      if (!ok) return;\n      const msg = el('uf-codex-msg');\n      try {\n        const r = await fetch(`/api/tokens/${tokenId}`, { method: 'DELETE', credentials: 'same-origin' });\n        if (!r.ok) throw new Error('Revoke failed');\n        if (msg) { msg.textContent = 'Revoked'; msg.style.color = 'var(--color-error)'; }\n        await renderList();\n        setTimeout(() => { formEl.style.display = 'none'; }, 350);\n      } catch (err) {\n        if (msg) { msg.textContent = err?.message || 'Revoke failed'; msg.style.color = 'var(--red)'; }\n      }\n    });\n\n    const _autoCreateCodex = async () => {\n      const msg = el('uf-codex-msg');\n      const prompt = el('uf-codex-prompt');\n      const pending = el('uf-codex-pending');\n      const createBtn = el('uf-codex-create-btn');\n      if (prompt) prompt.style.display = 'none';\n      if (createBtn) createBtn.style.display = 'none';\n      // Whirlpool spinner while the POST is in flight.\n      let _wp = null;\n      if (pending) {","sourceCodeStart":5370,"sourceCodeEnd":5406,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/settings.js#L5370-L5406","documentation":"Thrown when DELETE /api/tokens/{tokenId} (Revoke button) returns non-2xx. Unlike the PATCH handlers this handler never parses the response body, so the user always sees the generic 'Revoke failed' regardless of whether the server said 401, 404, or 403. The backend (routes/api_token_routes.py) returns 404 when the token id no longer exists and does not invalidate the cache in that case.","triggerScenarios":"Clicking Revoke after confirming the styled dialog: DELETE /api/tokens/{formEl.dataset.createdTokenId}. Produces 404 if the token was already revoked (double-click, or revoked elsewhere), 401/403 if the session expired or the user lacks rights, 500 on server fault.","commonSituations":"Double-invoking Revoke (button not disabled while the request is in flight); token deleted from another session; auth cookie expired between opening settings and clicking Revoke.","solutions":["Treat 404 as success-or-idempotent: the token is already gone, so refresh the list instead of showing an error.","Parse the response body (d.detail) like the PATCH handlers do, and include r.status in the fallback message.","Disable the Revoke button while the DELETE is in flight to prevent double-fire.","For 401, redirect to login and let the user retry after re-auth."],"exampleFix":"// before\nconst r = await fetch(`/api/tokens/${tokenId}`, { method: 'DELETE', credentials: 'same-origin' });\nif (!r.ok) throw new Error('Revoke failed');\n// after\nconst r = await fetch(`/api/tokens/${tokenId}`, { method: 'DELETE', credentials: 'same-origin' });\nconst d = await r.json().catch(() => ({}));\nif (r.status === 404) { /* already revoked */ }\nelse if (!r.ok) throw new Error(d.detail || `Revoke failed (HTTP ${r.status})`);","handlingStrategy":"try-catch","validationCode":"if (!formEl.dataset.createdTokenId) return; // already present; add a re-check right before fetch in case Revoke ran","typeGuard":null,"tryCatchPattern":"try { const r = await fetch(`/api/tokens/${tokenId}`, { method: 'DELETE', credentials: 'same-origin' }); const d = await r.json().catch(() => ({})); if (r.status === 404) { /* already revoked */ } else if (!r.ok) throw new Error(d.detail || `Revoke failed (HTTP ${r.status})`); } catch (err) { msg.textContent = err?.message || 'Revoke failed'; }","preventionTips":["Treat DELETE 404 as idempotent success for revoke-style operations.","Disable the confirm/Revoke button while the DELETE is in flight.","Parse the error body for detail like the PATCH handlers do.","Invalidate createdTokenId immediately after a successful revoke."],"tags":["fetch","api","http","tokens","idempotency"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}