decolua/9router · info · Error
tunnel cancelled
Error message
tunnel cancelled
What it means
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.
Source
Thrown at src/lib/tunnel/cloudflare/manager.js:31
};
export function getTunnelService() { return svc; }
export function isTunnelManuallyDisabled() { return svc.cancelToken.cancelled; }
export function isTunnelReconnecting() { return svc.spawnInProgress; }
let onUnexpectedExit = null;
export function setTunnelUnexpectedExitCallback(cb) { onUnexpectedExit = cb; }
async function registerTunnelUrl(shortId, tunnelUrl) {
await fetch(`${WORKER_URL}/api/tunnel/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ shortId, tunnelUrl })
});
}
function throwIfCancelled(token) {
if (token.cancelled) throw new Error("tunnel cancelled");
}
export async function enableTunnel(localPort = 20128) {
console.log(`[Tunnel] enable start (port=${localPort})`);
svc.cancelToken = { cancelled: false };
svc.activeLocalPort = localPort;
svc.spawnInProgress = true;
const token = svc.cancelToken;
try {
if (isCloudflaredRunning()) {
const existing = loadState();
if (existing?.tunnelUrl && existing?.shortId) {
const publicUrl = `https://r${existing.shortId}.abc-tunnel.us`;
// Reuse only if BOTH direct + public URL alive (avoid stale socket after network change)
const [directOk, publicOk] = await Promise.all([
probeUrlAlive(existing.tunnelUrl),
probeUrlAlive(publicUrl),View on GitHub (pinned to 90b52e06ff)
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.
Example fix
// before
await enableTunnel(20128);
// after
try {
await enableTunnel(20128);
} catch (e) {
if (e.message === "tunnel cancelled") return; // expected abort
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Check token state before starting a long enable if (svc.cancelToken?.cancelled) return; // a cancel is already pending
Type guard
const isTunnelCancel = (e) => e instanceof Error && e.message === "tunnel cancelled";
Try / catch
try {
await enableTunnel(port);
} catch (e) {
if (isTunnelCancel(e)) return; // normal user abort
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cancelled
- Unsupported platform: ${platform}
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- cancelled
- tailscale cancelled
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/c34896f9509f3058.
Report an issue: GitHub.