{"record":{"id":"c476628d7958aca6","repo":"decolua/9router","slug":"tailscale-cancelled","errorCode":null,"errorMessage":"tailscale cancelled","messagePattern":"tailscale cancelled","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"src/lib/tunnel/tailscale/manager.js","lineNumber":20,"sourceCode":"import { startFunnel, stopFunnel, isTailscaleRunning, isTailscaleRunningStrict, isTailscaleLoggedIn, isTailscaleLoggedInStrict, startLogin, startDaemonWithPassword, provisionCert } from \"./tailscale.js\";\nimport { waitForHealth } from \"./healthCheck.js\";\nimport { getSettings, updateSettings } from \"@/lib/localDb\";\nimport { getCachedPassword, loadEncryptedPassword, initDbHooks } from \"@/mitm/manager\";\n\ninitDbHooks(getSettings, updateSettings);\n\nconst svc = {\n  cancelToken: { cancelled: false },\n  spawnInProgress: false,\n  lastRestartAt: 0,\n  activeLocalPort: null,\n};\n\nexport function getTailscaleService() { return svc; }\nexport function isTailscaleReconnecting() { return svc.spawnInProgress; }\n\nfunction throwIfCancelled(token) {\n  if (token.cancelled) throw new Error(\"tailscale cancelled\");\n}\n\nexport async function enableTailscale(localPort = 20128) {\n  console.log(`[Tailscale] enable start (port=${localPort})`);\n  svc.cancelToken = { cancelled: false };\n  svc.activeLocalPort = localPort;\n  svc.spawnInProgress = true;\n  const token = svc.cancelToken;\n\n  try {\n    const sudoPass = getCachedPassword() || await loadEncryptedPassword() || \"\";\n    await startDaemonWithPassword(sudoPass);\n    console.log(\"[Tailscale] daemon ready\");\n    throwIfCancelled(token);\n\n    const existing = loadState();\n    const shortId = existing?.shortId || generateShortId();\n    const tsHostname = shortId;","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/tunnel/tailscale/manager.js#L2-L38","documentation":"The tailscale manager's throwIfCancelled() throws 'tailscale cancelled' when the service cancelToken is cancelled at a checkpoint inside enableTailscale(). It aborts in-progress tailscale setup (auth, spawn, health wait) promptly when the user disables the tunnel. This is cooperative cancellation, not a functional failure.","triggerScenarios":"disableTailscale() (or reconnect logic) sets svc.cancelToken.cancelled = true while enableTailscale() is still running; the next throwIfCancelled() checkpoint throws. Also occurs when a stale cancelled token is carried into a new enable attempt.","commonSituations":"Rapid off/on toggling of the tailscale tunnel in the dashboard; a reconnect cycle cancelling the original enable; concurrent enable/disable calls racing on the shared service token.","solutions":["Catch message === 'tailscale cancelled' in callers and treat as a normal abort.","Create a fresh cancelToken at the start of each enableTailscale() call.","Serialize enable/disable/reconnect operations so they don't interleave on svc.cancelToken.","Inspect svc state (getTailscaleService(), isTailscaleReconnecting()) before re-enabling after a cancellation."],"exampleFix":"// before\nawait enableTailscale(20128);\n// after\ntry {\n  await enableTailscale(20128);\n} catch (e) {\n  if (e.message === \"tailscale cancelled\") return;\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (getTailscaleService().cancelToken?.cancelled) return; // cancel pending — don't enable","typeGuard":"const isTailscaleCancel = (e) => e instanceof Error && e.message === \"tailscale cancelled\";","tryCatchPattern":"try {\n  await enableTailscale(port);\n} catch (e) {\n  if (isTailscaleCancel(e)) return; // expected abort\n  throw e;\n}","preventionTips":["Reset the token ({ cancelled: false }) at the start of each enable attempt.","Guard enable/disable with a mutex so reconnect can't cancel a fresh enable.","Check isTailscaleReconnecting() before issuing enable.","Map 'tailscale cancelled' to a no-op in user-facing error handling."],"tags":["cancellation","tailscale","tunnel"],"backgroundTag":"operation-cancelled","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}