decolua/9router · info · Error
tailscale cancelled
Error message
tailscale cancelled
What it means
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.
Source
Thrown at src/lib/tunnel/tailscale/manager.js:20
import { startFunnel, stopFunnel, isTailscaleRunning, isTailscaleRunningStrict, isTailscaleLoggedIn, isTailscaleLoggedInStrict, startLogin, startDaemonWithPassword, provisionCert } from "./tailscale.js";
import { waitForHealth } from "./healthCheck.js";
import { getSettings, updateSettings } from "@/lib/localDb";
import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
initDbHooks(getSettings, updateSettings);
const svc = {
cancelToken: { cancelled: false },
spawnInProgress: false,
lastRestartAt: 0,
activeLocalPort: null,
};
export function getTailscaleService() { return svc; }
export function isTailscaleReconnecting() { return svc.spawnInProgress; }
function throwIfCancelled(token) {
if (token.cancelled) throw new Error("tailscale cancelled");
}
export async function enableTailscale(localPort = 20128) {
console.log(`[Tailscale] enable start (port=${localPort})`);
svc.cancelToken = { cancelled: false };
svc.activeLocalPort = localPort;
svc.spawnInProgress = true;
const token = svc.cancelToken;
try {
const sudoPass = getCachedPassword() || await loadEncryptedPassword() || "";
await startDaemonWithPassword(sudoPass);
console.log("[Tailscale] daemon ready");
throwIfCancelled(token);
const existing = loadState();
const shortId = existing?.shortId || generateShortId();
const tsHostname = shortId;View on GitHub (pinned to 90b52e06ff)
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.
Example fix
// before
await enableTailscale(20128);
// after
try {
await enableTailscale(20128);
} catch (e) {
if (e.message === "tailscale cancelled") return;
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (getTailscaleService().cancelToken?.cancelled) return; // cancel pending — don't enable
Type guard
const isTailscaleCancel = (e) => e instanceof Error && e.message === "tailscale cancelled";
Try / catch
try {
await enableTailscale(port);
} catch (e) {
if (isTailscaleCancel(e)) return; // expected abort
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cancelled
- cancelled
- tunnel cancelled
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- [Tailscale] health check timed out, will retry via watchdog
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/c476628d7958aca6.
Report an issue: GitHub.