decolua/9router · info · Error

cancelled

Error message

cancelled

What it means

Identical cooperative-cancellation mechanism as the cloudflare health check, implemented in the tailscale tunnel's waitForHealth(): while polling the funnel URL, a true cancelToken.cancelled throws 'cancelled' immediately. It signals that a caller (enableTailscale path) aborted the health wait.

Source

Thrown at src/lib/tunnel/tailscale/healthCheck.js:24

  let hostname;
  try { hostname = new URL(url).hostname; } catch { return false; }

  if (!await resolveDns(hostname, HEALTH_CHECK.dnsTimeoutMs)) return false;

  try {
    const res = await fetch(`${url}/api/health`, {
      signal: AbortSignal.timeout(HEALTH_CHECK.fetchTimeoutMs),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export async function waitForHealth(url, cancelToken = { cancelled: false }) {
  const start = Date.now();
  while (Date.now() - start < HEALTH_CHECK.timeoutMs) {
    if (cancelToken.cancelled) throw new Error("cancelled");
    if (await probeUrlAlive(url)) return true;
    await new Promise((r) => setTimeout(r, HEALTH_CHECK.intervalMs));
  }
  throw new Error(`Health check timeout after ${HEALTH_CHECK.timeoutMs}ms`);
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Handle 'cancelled' as expected control flow in the caller, not as an error to report.
  2. Ensure each enableTailscale() attempt creates a fresh { cancelled: false } token.
  3. Avoid sharing one token object between concurrent enable/disable paths.
  4. Defer disable until the enable/health phase completes, or lock the toggle during startup.

Example fix

// before
await waitForHealth(url, token);
// after
try {
  await waitForHealth(url, token);
} catch (e) {
  if (e.message === "cancelled") return;
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const cancelToken = { cancelled: false }; // fresh per attempt
if (cancelToken.cancelled) return;

Type guard

const isCancelError = (e) => e instanceof Error && e.message === "cancelled";

Try / catch

try {
  await waitForHealth(funnelUrl, token);
} catch (e) {
  if (isCancelError(e)) return;
  throw e;
}

Prevention

When it happens

Trigger: User disables the tailscale tunnel while enableTailscale() is waiting for the funnel URL to become healthy; the manager sets cancelToken.cancelled = true and the next loop iteration throws. Reusing a cancelled token object across attempts also triggers it instantly.

Common situations: Toggling tailscale off during startup in the dashboard; reconnect logic resetting the token mid-wait; shared token state left cancelled from a previous attempt.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/1e55ef49f6643a7b. Report an issue: GitHub.