decolua/9router · info · Error

cancelled

Error message

cancelled

What it means

waitForHealth() polls a tunnel URL until it responds, bounded by HEALTH_CHECK.timeoutMs. Between polls it checks the caller-supplied cancelToken; if token.cancelled is true it throws 'cancelled' immediately instead of continuing to poll. This is a cooperative-cancellation signal, not a failure of the health check itself — some upstream code (enableTunnel) cancelled the wait.

Source

Thrown at src/lib/tunnel/cloudflare/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. Treat 'cancelled' as an expected control-flow signal — catch it and stop silently rather than surfacing as an error.
  2. Ensure a fresh cancelToken ({ cancelled: false }) is created for every enableTunnel() call (the manager does this in enableTunnel).
  3. If you see it spuriously, check whether code shares one token object across enable/disable calls.
  4. Wait for the health check to complete before issuing a disable, or gate disable UI during startup.

Example fix

// before
await waitForHealth(url, sharedToken);
// after
const token = { cancelled: false }; // fresh token per attempt
try {
  await waitForHealth(url, token);
} catch (e) {
  if (e.message === "cancelled") return; // user cancelled — not an error
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure a fresh token before waiting
const cancelToken = { cancelled: false };
if (cancelToken.cancelled) return; // don't even start

Type guard

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

Try / catch

try {
  await waitForHealth(url, token);
} catch (e) {
  if (isCancelError(e)) return; // expected abort
  throw e;
}

Prevention

When it happens

Trigger: User disables the tunnel or navigates away while enableTunnel() is still waiting for the cloudflared tunnel URL to become healthy; the manager sets cancelToken.cancelled = true and the next poll iteration throws. Also thrown if the same shared cancelToken object is reused across calls with cancelled already true.

Common situations: Clicking 'disable tunnel' in the dashboard during startup; a reconnect flow resetting the token mid-wait; or stale token reuse causing an immediate 'cancelled' even though no one cancelled this attempt.

Related errors


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