On an interactive server circuit, NavigationManager.NavigateTo(uri) sends the browser a JS call from the framework's own fire-and-forget task and awaits the acknowledgement. When the circuit's connection drops first, the circuit is kept for reconnection, so it is not yet permanently disconnected, and the framework's quiet path for a disconnected circuit does not apply. The JS-interop call timeout cancels the call a minute later. The framework then logs Navigation failed when changing the location to … and raises the cancellation as unhandled, which the circuit host logs as CircuitHost[111] Unhandled exception in circuit and answers by ending the circuit. The task is the framework's, so no caller can catch it (MeshWeaver#5362).

Measured on 2026-10-23: two recurrences, on two pods, went to /ReinsuranceDemo/Subscribe and /SST/Subscribe. That is the paywall a gated page redirects an unentitled viewer to (PluginGate sets the plugin's RedirectOnDenied, and NamedAreaView renders a RedirectControl). RedirectView issued that redirect from a data emission, at a browser whose connection may already have dropped. The earlier full-page shapes are covered by links that leave the app (FullPageNavigation.LeaveTo).

The fix

InCircuitNavigation.NavigateWithin (MeshWeaver.Blazor) asks the browser itself through Blazor.navigateTo, the browser-side entry of the same navigation, which still consults the circuit's location-changing handlers. It then classifies how that call ended against the circuit's own connection record, ICircuitConnection. That record is fed by CircuitConnection, a scoped CircuitHandler in MeshWeaver.Hosting.Blazor that counts every OnConnectionDown and tracks the current state.

The call ended… and the connection… logged as
acknowledged anything nothing
JSDisconnectedException (the circuit is gone) anything Debug
cancelled was down when the call was issued, dropped while it was pending (even if it came back), or is down now Information; the navigation is issued again the moment the connection is restored (see below)
cancelled stayed up the whole time Error: a live browser did not answer within the call timeout
cancelled is not reported by the host Error: no evidence of a disconnect
any other failure (JSException …) anything Error

None of these ends the circuit.

A lost navigation is issued again on reconnection. The framework used to end the circuit, so a reconnecting browser reloaded and the redirect ran again. Now the circuit is kept, and a navigation the browser never received must still reach it. Otherwise a viewer who reconnects stays on a page that renders nothing but the redirect. Each send watches ICircuitConnection.Restored from before it goes out. If the connection comes back while the call is still pending, the call went out over a connection that dropped and will never be answered, so the helper sends the same navigation again at once. It does not wait for the lost call's JS-interop timeout. The browser run on #4135 measured that wait at about 60 s on a blank page. The lost call's later timeout is then only logged, and the navigation is not sent a third time. A call that was answered before the restoration is not sent again. The helper skips the re-send if the navigation was superseded first:

A circuit that ends without reconnecting completes Restored, and nothing is sent.

Only a restoration that ends a drop this call was lost to triggers the re-send. The circuit marks the connection up and publishes Restored outside any navigation turn, so a navigation can go out over the already restored connection before that restoration is published. Each send therefore records the connection state it was sent under (Drops, IsConnected), and a restoration counts only when the connection was down at the send or has dropped since. Any other restoration is ignored and does not use up the watch, so a later real drop still re-sends.

Ticket and send are one navigation turn (ICircuitConnection.NavigationTurn, a plain monitor over synchronous work only: a turn sends the call and returns, and never awaits the answer). A fresh navigation takes its ticket and is sent in one turn. A replay checks its ticket and is sent in one turn. A newer navigation therefore cannot be sent between a replay's check and its send, and the newest destination always reaches the browser last. A location move made by the viewer comes from the browser and is not serialized by the turn. The replay reads it as late as it can.

An empty URI means the application base, as it does for NavigationManager.NavigateTo(""). A statically rendered component has no browser to call, so it keeps the framework's navigation (an HTTP redirect), synchronously.

Routed through it: RedirectView, ButtonView's NavigateToHref and NavigateOnAccepted, and NavigationService.NavigateTo for an in-app (non-forceLoad) navigation. Other direct NavigationManager.NavigateTo calls are not migrated yet. They still take the framework's call, mostly from user clicks where the race window is a single round trip.

Proof

InCircuitNavigationTest (MeshWeaver.Hosting.Blazor.Test):

BoundRedirectNavigatesTest.AViewerWhoseConnectionDropped_IsNotAnUnhandledCircuitException (MeshWeaver.Blazor.Views.Test) renders a real RedirectView. It fails when RedirectView calls NavigationManager.NavigateTo, because the redirect then reaches the framework's navigation and not the browser. ARedirectLostToADroppedConnection_ReachesTheBrowserWhenItReconnects renders the same view through a drop and a reconnection.