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
- Handle 'cancelled' as expected control flow in the caller, not as an error to report.
- Ensure each enableTailscale() attempt creates a fresh { cancelled: false } token.
- Avoid sharing one token object between concurrent enable/disable paths.
- 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
- Use one cancel token per attempt, created fresh in enableTailscale().
- Route all cancellations through the manager's token so state stays consistent.
- Log cancels at info level, not error level.
- Don't run disable and enable concurrently on the same service.
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
- cancelled
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- tailscale cancelled
- [Tailscale] health check timed out, will retry via watchdog
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/1e55ef49f6643a7b.
Report an issue: GitHub.