{"record":{"id":"e3519d20a69667cd","repo":"decolua/9router","slug":"remove-failed","errorCode":null,"errorMessage":"Remove failed","messagePattern":"Remove failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js","lineNumber":280,"sourceCode":"      setExtrasActionError(e.message);\n    } finally {\n      stopLogPolling();\n      setExtrasActionLoading(false);\n    }\n  }, [pendingExtras, startLogPolling, stopLogPolling]);\n\n  const removeExtraConfirmed = useCallback(async (extra) => {\n    setRemovingExtra(extra);\n    setExtrasActionError(\"\");\n    startLogPolling();\n    try {\n      const res = await fetch(\"/api/headroom/extras\", {\n        method: \"DELETE\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ extras: [extra] }),\n      });\n      const data = await res.json().catch(() => ({}));\n      if (!res.ok) throw new Error(data.error || \"Remove failed\");\n      setHeadroomExtras((s) => ({\n        ...s,\n        version: data.version ?? s.version,\n        extras: data.extras || s.extras,\n      }));\n    } catch (e) {\n      setExtrasActionError(e.message);\n    } finally {\n      stopLogPolling();\n      setRemovingExtra(null);\n    }\n  }, [startLogPolling, stopLogPolling]);\n\n  const handleInstallExtras = useCallback(() => {\n    if (pendingExtras.length === 0) return;\n    // Warn about the heavy ~1GB torch download before installing [ml].\n    if (pendingExtras.includes(\"ml\")) {\n      setExtrasConfirm({","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js#L262-L298","documentation":"TokenSaverClient removes a Headroom compression extra by DELETEing to /api/headroom/extras. When the route responds with a non-OK status it returns { error, code } — e.g. NO_PYTHON (no Python 3.10 found), INVALID_EXTRAS, or a generic 500 from pip uninstall failing — and the client falls back to the literal 'Remove failed' message when data.error is absent. It signals the uninstall of the extra did not complete server-side.","triggerScenarios":"DELETE /api/headroom/extras with body { extras: [name] } returns 400 when uninstallHeadroomExtras throws NO_PYTHON or INVALID_EXTRAS, or 500 when the pip uninstall subprocess fails; the dashboard then throws this error in the catch block at TokenSaverClient.js:280.","commonSituations":"No suitable Python 3.10 interpreter on the machine; typo'd or already-uninstalled extra name; pip uninstall permission errors; server not running so fetch fails and data defaults to {}.","solutions":["Check that a Python 3.10 interpreter is installed and discoverable (the route requires findPython310() to succeed).","Reload the extras list and confirm the extra is actually still installed before retrying the remove.","Read the install log via GET /api/headroom/extras?log=1 to see the pip uninstall failure reason.","Retry the DELETE; transient pip/network failures during uninstall can resolve on a second attempt."],"exampleFix":"// before\nif (!res.ok) throw new Error(data.error || \"Remove failed\");\n// after\nif (!res.ok) throw new Error(data.error ? `${data.error}${data.code ? ` (${data.code})` : \"\"}` : `Remove failed (HTTP ${res.status})`);","handlingStrategy":"try-catch","validationCode":"if (!Array.isArray(extras) || extras.some((e) => typeof e !== \"string\" || !e.trim())) throw new Error(\"extras must be non-empty strings\");\nconst status = await fetch(\"/api/headroom/extras\").then((r) => r.json()).catch(() => null);\nif (!status?.pythonAvailable) throw new Error(\"Python 3.10 not found — remove requires a Python interpreter\");","typeGuard":"const isApiError = (d) => d && typeof d === \"object\" && typeof d.error === \"string\";","tryCatchPattern":"try {\n  const res = await fetch(\"/api/headroom/extras\", { method: \"DELETE\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ extras: [extra] }) });\n  const data = await res.json().catch(() => ({}));\n  if (!res.ok) throw new Error(data.error || `Remove failed (HTTP ${res.status})`);\n} catch (e) {\n  console.warn(\"headroom extra remove failed:\", e.message);\n  await refreshExtras();\n}","preventionTips":["Confirm Python 3.10 is installed before managing extras.","Always re-fetch the extras list after a failed remove instead of trusting local state.","Surface data.code (NO_PYTHON, INVALID_EXTRAS) to the user, not just a generic message."],"tags":["http-error","python","ui-error"],"backgroundTag":"http-4xx-client-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}