Skip to content

Follow links when the interactive router is disconnected - #68562

Draft
oguzozshn wants to merge 1 commit into
dotnet:mainfrom
oguzozshn:fix-9964-follow-links-when-disconnected
Draft

Follow links when the interactive router is disconnected#68562
oguzozshn wants to merge 1 commit into
dotnet:mainfrom
oguzozshn:fix-9964-follow-links-when-disconnected

Conversation

@oguzozshn

Copy link
Copy Markdown
  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making.

Fall back to a full page load when the interactive router is disconnected.

Opened as a draft on purpose. The contributor guide asks for a positive acknowledgement of
the approach before investing in an implementation, and this issue has an open design question
(see Design question below). The code is here so the proposal is concrete rather than
hypothetical — I'll move it out of draft if the approach is acceptable, or rework it if not.

Description

Link clicks are intercepted whenever an interactive router is registered, without considering
whether that router can actually be reached:

eventDelegator.notifyAfterClick(event => {
  if (!hasInteractiveRouter()) {
    return;
  }

  handleClickForNavigationInterception(event, absoluteInternalHref => {
    performInternalNavigation(absoluteInternalHref, /* interceptedLink */ true, /* replace */ false);
  });
});

With the Blazor Server circuit down, performInternalNavigation ends in notifyLocationChanged,
which dispatches to .NET over the circuit and fails with
Cannot send data if the connection is not in the 'Connected' State. Because
handleClickForNavigationInterception has already called preventDefault(), the browser's own
navigation is suppressed too, so the user is left on the page with nothing but a console error.

This change lets navigations fall back to a full page load while the interactive router is
unreachable, which follows the link and gives the app a chance to establish a new circuit. The
mechanism for that already exists — currentPageLoadMechanism() can return
'serverside-fullpageload', and navigateToCore routes that to performExternalNavigation — so
this mostly widens the condition that selects it.

Changes

  • NavigationUtils — adds an optional connection checker, following the same registration
    pattern as attachProgrammaticEnhancedNavigationHandler. Rendering modes that never register one
    (i.e. WebAssembly) report as connected, so their behavior is unchanged.
  • NavigationManager — skips click interception without calling preventDefault(), so the
    browser performs the navigation itself; and returns 'serverside-fullpageload' from
    currentPageLoadMechanism() so programmatic Blazor.navigateTo is covered too.
  • CircuitManager — exposes isConnected(), the public form of a HubConnectionState check
    already made in four places in the class.
  • Boot.Server.Common — registers the checker. It reads the module-level circuit, so it keeps
    reporting the current one after a reconnection replaces the instance.

38 lines of product code across 4 files. No public API change.

Design question

A full page load discards circuit state, which is what the reconnection UI exists to avoid. My
reasoning for accepting that here: this path is only reached after the user clicked a link to
another page, so the current page's state is being left behind either way — and today the
alternative isn't "state is preserved", it's a swallowed navigation plus an unhandled error.

If you'd rather this were opt-in, or gated on the reconnection attempts having been exhausted
rather than on the connection simply not being Connected right now, I'm happy to rework it.

Scope

Deliberately limited to link clicks and programmatic navigation. Back/forward (onPopState) reaches
the same interop call while disconnected, but the right behavior there is less obvious, so I left it
for a separate change.

Testing

  • Added src/Components/Web.JS/test/NavigationManager.test.ts, covering both directions: clicks are
    intercepted while connected, and left to the browser while disconnected. Reverting the fix fails
    only the disconnected case.
  • Full Web.JS suite passes (12 suites, 251 tests).
  • npm run build:debug succeeds for all four bundles, including Boot.WebAssembly.
  • ESLint reports no new errors against the main baseline for the touched files.

Not yet covered: an E2E test, and manual verification against a real disconnected circuit.

Fixes #9964

Link clicks were intercepted whenever an interactive router was registered,
without considering whether that router could be reached. With the Blazor Server
circuit down, performInternalNavigation dispatched to .NET over the circuit and
failed with "Cannot send data if the connection is not in the 'Connected'
State" - after preventDefault had already suppressed the browser's own
navigation, leaving the user stuck on the page.

Navigations now fall back to a full page load while the interactive router is
unreachable. That follows the link and gives the app a chance to establish a new
circuit.

- NavigationUtils: add an optional connection checker, following the existing
  attachProgrammaticEnhancedNavigationHandler registration pattern. Rendering
  modes that never register one (i.e. WebAssembly) report as connected, so their
  behavior is unchanged.
- NavigationManager: skip click interception without calling preventDefault, and
  return 'serverside-fullpageload' from currentPageLoadMechanism so programmatic
  Blazor.navigateTo is covered too.
- CircuitManager: expose isConnected().
- Boot.Server.Common: register the checker. It reads the module-level circuit, so
  it keeps reporting the current one after a reconnection replaces the instance.

Back/forward navigation (onPopState) hits the same interop call while
disconnected and is intentionally left out of this change.

Fixes dotnet#9964
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 16, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Thanks for your PR, @oguzozshn. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@oguzozshn

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@oguzozshn

Copy link
Copy Markdown
Author

I looked into the failing checks — as far as I can tell none of them are caused by this change, but flagging what I found so a reviewer doesn't have to repeat the work.

Blazor E2E tests on Linux (Mono) — 2 failures out of 2535:

  • ServerVirtualizationTest.QuickGrid_ScrollToItem_NegativeIndex_ScrollsToTop — this is Quarantine VirtualizationTest.QuickGrid_ScrollToItem_NegativeIndex_ScrollsToTop #68559, with the same assertion message (topRendered=146, scrollTop=30). That issue reports 15 failures over the past 30 days across main CI and merged PR validation.
  • ServerVirtualizationTest.ScrollToItem_AfterEndJump_LandsAtTarget — no issue that I could find, but it's the same class and the same scroll-position assertion, and AnchorMode_DeleteAboveViewport_ViewportStaysStable was already skipped as quarantined in this run.

Build Test: Windows local development validation — a file lock during static web asset compression:

Compression.targets(359,5): error : The process cannot access the file
'...\Microsoft.AspNetCore.Components.WebAssembly\...\4iw531pjru-{0}-5poccmhjvu-5poccmhjvu.br'
because it is being used by another process.

Build Monitor Helix Jobs — work item batch_4--net11.0 failed on osx.26.arm64, ubuntu.2404.amd64 and windows.amd64.vs2026.

Both of those last two also occur on #68558, which only touches
src/DataProtection/Extensions/test/DataProtectionProviderTests.cs — same work item, same .br path — so they look like they're coming from the shared baseline rather than from either change.

For what it's worth on the substance: none of the failing tests exercise navigation, and this change is a no-op while the circuit is connected — isInteractiveRouterConnected() returns true and the existing 'clientside-router' path is taken unchanged.

Happy to rebase if that would help rerun against a greener baseline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow links when disconnecting

1 participant