{"record":{"id":"c34896f9509f3058","repo":"decolua/9router","slug":"tunnel-cancelled","errorCode":null,"errorMessage":"tunnel cancelled","messagePattern":"tunnel cancelled","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"src/lib/tunnel/cloudflare/manager.js","lineNumber":31,"sourceCode":"};\n\nexport function getTunnelService() { return svc; }\nexport function isTunnelManuallyDisabled() { return svc.cancelToken.cancelled; }\nexport function isTunnelReconnecting() { return svc.spawnInProgress; }\n\nlet onUnexpectedExit = null;\nexport function setTunnelUnexpectedExitCallback(cb) { onUnexpectedExit = cb; }\n\nasync function registerTunnelUrl(shortId, tunnelUrl) {\n  await fetch(`${WORKER_URL}/api/tunnel/register`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ shortId, tunnelUrl })\n  });\n}\n\nfunction throwIfCancelled(token) {\n  if (token.cancelled) throw new Error(\"tunnel cancelled\");\n}\n\nexport async function enableTunnel(localPort = 20128) {\n  console.log(`[Tunnel] 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    if (isCloudflaredRunning()) {\n      const existing = loadState();\n      if (existing?.tunnelUrl && existing?.shortId) {\n        const publicUrl = `https://r${existing.shortId}.abc-tunnel.us`;\n        // Reuse only if BOTH direct + public URL alive (avoid stale socket after network change)\n        const [directOk, publicOk] = await Promise.all([\n          probeUrlAlive(existing.tunnelUrl),\n          probeUrlAlive(publicUrl),","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/tunnel/cloudflare/manager.js#L13-L49","documentation":"throwIfCancelled() checks the tunnel service's cancelToken at key steps inside enableTunnel() and throws 'tunnel cancelled' when token.cancelled is true. It lets long-running tunnel setup (download, spawn, register, health wait) abort promptly when the user disables the tunnel mid-flight. Like the health-check 'cancelled' error, it is cooperative cancellation, not an operational failure.","triggerScenarios":"User (or the dashboard) triggers disableTunnel() while enableTunnel() is still executing; the next throwIfCancelled() checkpoint in enableTunnel observes cancelled=true and throws. Also occurs if a shared/reset token from a previous run is left cancelled when re-entering the flow.","commonSituations":"Toggling the tunnel off and on quickly in the dashboard — the disable cancels the in-progress enable; timeouts in upstream UI logic cancelling setup; concurrent enable/disable calls racing on svc.cancelToken.","solutions":["Catch errors whose message is 'tunnel cancelled' in the caller and treat them as a normal abort (no user-facing error).","Create a fresh cancelToken at the start of every enableTunnel() call (default behavior) and avoid sharing tokens across attempts.","Serialize enable/disable operations (queue or lock) so a disable can't interleave with an in-flight enable.","Check svc.cancelToken state before retrying enable after a cancellation."],"exampleFix":"// before\nawait enableTunnel(20128);\n// after\ntry {\n  await enableTunnel(20128);\n} catch (e) {\n  if (e.message === \"tunnel cancelled\") return; // expected abort\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Check token state before starting a long enable\nif (svc.cancelToken?.cancelled) return; // a cancel is already pending","typeGuard":"const isTunnelCancel = (e) => e instanceof Error && e.message === \"tunnel cancelled\";","tryCatchPattern":"try {\n  await enableTunnel(port);\n} catch (e) {\n  if (isTunnelCancel(e)) return; // normal user abort\n  throw e;\n}","preventionTips":["Serialize enable/disable with a simple lock/queue to prevent interleaving.","Always reset svc.cancelToken = { cancelled: false } at the top of enable.","Treat 'tunnel cancelled' as success-path cleanup in UI code.","Debounce rapid toggle clicks in the dashboard."],"tags":["cancellation","cloudflare","tunnel"],"backgroundTag":"operation-cancelled","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}