dotnet/aspnetcore · critical

Only one interactive runtime may enable navigation intercept

Error message

Only one interactive runtime may enable navigation interception at a time.

What it means

Thrown by setHasInteractiveRouter() in NavigationUtils.ts:154 when a second interactive Blazor renderer tries to register as the navigation-intercepting router. Blazor's client-side navigation interception (click handling on anchors, popstate, etc.) can only be owned by a single interactive runtime, because two routers would race to interpret and handle the same navigation events. The guard compares the already-registered rendererId against the new one and throws if they differ.

Source

Thrown at src/Components/Web.JS/src/Services/NavigationUtils.ts:154

      if (candidate instanceof HTMLAnchorElement || candidate instanceof SVGAElement) {
        return candidate;
      }
    }
  }
  return null;
}

export function hasInteractiveRouter(): boolean {
  return interactiveRouterRendererId !== undefined;
}

export function getInteractiveRouterRendererId() : WebRendererId | undefined {
  return interactiveRouterRendererId;
}

export function setHasInteractiveRouter(rendererId: WebRendererId) {
  if (interactiveRouterRendererId !== undefined && interactiveRouterRendererId !== rendererId) {
    throw new Error('Only one interactive runtime may enable navigation interception at a time.');
  }

  interactiveRouterRendererId = rendererId;
}

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Ensure only one interactive runtime owns routing — for InteractiveAuto, the WebAssembly runtime should replace (not add to) the Server runtime; verify you are not double-registering both in production.
  2. If you have a legitimate need for multiple runtimes, design only one of them to handle navigation and keep the others from calling setHasInteractiveRouter.
  3. Check for accidental double script inclusion of blazor.web.js / blazor.webassembly.js in the host page.
  4. On teardown or before re-init, call the equivalent reset path so interactiveRouterRendererId returns to undefined before a different renderer registers.

Example fix

// before — both runtimes call this
setHasInteractiveRouter(WebRendererId.Server);
setHasInteractiveRouter(WebRendererId.WebAssembly); // throws

// after — only the active router registers; the wasm runtime takes over after server relinquishes
if (!hasInteractiveRouter()) {
  setHasInteractiveRouter(currentRendererId);
}
Defensive patterns

Strategy: validation

Validate before calling

import { hasInteractiveRouter } from './NavigationUtils';
function safeSetInteractiveRouter(id: WebRendererId) {
  if (hasInteractiveRouter()) {
    console.warn('Navigation interception already enabled; skipping re-registration.');
    return;
  }
  setHasInteractiveRouter(id);
}

Type guard

function canRegisterRouter(rendererId: WebRendererId): boolean {
  return !hasInteractiveRouter();
}

Prevention

When it happens

Trigger: Calling setHasInteractiveRouter(rendererId) a second time with a different WebRendererId while a previous rendererId is already registered (interactiveRouterRendererId !== undefined && !=== rendererId). Happens when both InteractiveServer and InteractiveWebAssembly rendermodes initialize their routers in the same document.

Common situations: Blazor Web App with InteractiveAuto rendermode transitioning from Server to WebAssembly where both runtimes start and each calls setHasInteractiveRouter. Manually bootstrapping multiple Blazor runtimes on one page. Hot-reload or re-initialization during development that re-runs startup without the first runtime releasing control. Misconfigured custom hosting that loads two blazor.*.js runtimes.

Related errors


AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06). Data as JSON: /api/errors/86efb65d6eba57a2. Report an issue: GitHub.