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

  1. Catch errors whose message is 'tunnel cancelled' in the caller and treat them as a normal abort (no user-facing error).
  2. Create a fresh cancelToken at the start of every enableTunnel() call (default behavior) and avoid sharing tokens across attempts.
  3. Serialize enable/disable operations (queue or lock) so a disable can't interleave with an in-flight enable.
  4. 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

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


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