Skip to content

Conversation

@javiercn
Copy link
Member

Fix Blazor navigation error caused by metadata comments in logical tree

Prevent metadata comments from becoming logical elements during rendering

Description

This PR fixes a bug where Blazor metadata comments (such as WebAssembly options, component state, and initializers) were being converted into logical elements during the initial render. When these comments were later removed from the DOM during component discovery, they remained in the logical tree as orphaned references, causing insertBefore errors during subsequent navigation between different render modes.

The fix modifies toLogicalElement() in LogicalElements.ts to skip metadata comments before they enter the logical tree, using a new isMetadataComment() helper function in ComponentDescriptorDiscovery.ts that identifies all Blazor metadata comment types.

Fixes #64522, #64472

Customer Impact

Users experience JavaScript errors and navigation failures when navigating between pages with different render modes (InteractiveServer, InteractiveWebAssembly, or InteractiveAuto) in Blazor Web Apps. The error manifests as:

  • Uncaught TypeError: Cannot read properties of null (reading 'insertBefore')
  • Failed page transitions requiring full browser refresh
  • Broken user experience in production applications

This particularly affects applications using:

  • PageTitle components with different render modes
  • Navigation between InteractiveWebAssembly and InteractiveServer pages
  • Any scenario mixing render modes in a Blazor United application

Regression?

  • Yes
  • No

This is a regression introduced in .NET 10.0 when the WebAssembly options discovery mechanism was added. The <!--Blazor-WebAssembly:{...}--> comment is new to v10.0 and was not present in v9.0. The toLogicalElement() function was indiscriminately converting all nodes including these new metadata comments into logical elements, which caused the orphaning issue when the comments were removed from the DOM.

Risk

  • High
  • Medium
  • Low

Justification:

  • The fix is surgical and only affects the initial logical tree construction
  • It prevents a specific category of nodes (metadata comments) from entering the logical tree
  • Metadata comments were never intended to be logical elements - they are consumed during discovery
  • The fix aligns behavior with design intent: these comments are transient markers, not components
  • Extensively validated with two independent reproduction cases from separate bug reports
  • No changes to core rendering logic, only to node filtering during tree construction
  • The isMetadataComment() function uses explicit prefix matching with well-defined comment formats

Verification

  • Manual (required)
  • Automated

Manual verification completed:

  1. BlazorApp10 (Issue .Net 10 Blazor Enhanced Navigation in WASM fails due to no PageTitle #64522): Validated navigation between InteractiveServer pages with/without PageTitle components, and InteractiveWebAssembly pages. Multiple navigation cycles completed without errors.

  2. BugReportApp (Issue .NET 10 Blazor navigation from wasm to interactive server broken #64472): Validated navigation between InteractiveWebAssembly → InteractiveServer → InteractiveWebAssembly. Both directions work correctly with SignalR connections established properly.

Packaging changes reviewed?

  • Yes
  • No
  • N/A

This is a JavaScript/TypeScript code change only. No packaging changes required.


When servicing release/2.3

  • Make necessary changes in eng/PatchConfig.props

@javiercn javiercn requested a review from a team as a code owner November 25, 2025 17:50
Copilot AI review requested due to automatic review settings November 25, 2025 17:50
@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Nov 25, 2025
@javiercn javiercn enabled auto-merge (squash) November 25, 2025 17:51
@javiercn
Copy link
Member Author

/backport to release/10.0

@github-actions
Copy link
Contributor

Started backporting to release/10.0 (link to workflow run)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical Blazor navigation bug where metadata comments (WebAssembly options, component state, and initializers) were incorrectly being converted into logical elements during initial rendering. When these comments were later removed from the DOM during component discovery, they remained in the logical tree as orphaned references, causing insertBefore errors during subsequent navigation between different render modes.

Key changes:

  • Added isMetadataComment() helper function to identify all Blazor metadata comment types
  • Modified toLogicalElement() to skip metadata comments during logical tree construction
  • Prevents orphaned references that cause navigation failures in Blazor Web Apps with mixed render modes

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts Adds isMetadataComment() function to identify metadata comments (Blazor-Server-Component-State, Blazor-WebAssembly-Component-State, Blazor-Web-Initializers, Blazor-WebAssembly) that should not become logical elements
src/Components/Web.JS/src/Rendering/LogicalElements.ts Filters out metadata comments in toLogicalElement() to prevent them from entering the logical tree during initial render

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment on lines +11 to +14
return content.trim().startsWith('Blazor-Server-Component-State:') ||
content.trim().startsWith('Blazor-WebAssembly-Component-State:') ||
content.trim().startsWith('Blazor-Web-Initializers:') ||
content.trim().startsWith('Blazor-WebAssembly:');
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The content.trim() call is repeated up to 4 times in the boolean expression. Store the trimmed content in a variable to avoid redundant string operations:

const content = node.textContent || '';
const trimmedContent = content.trim();
return trimmedContent.startsWith('Blazor-Server-Component-State:') ||
       trimmedContent.startsWith('Blazor-WebAssembly-Component-State:') ||
       trimmedContent.startsWith('Blazor-Web-Initializers:') ||
       trimmedContent.startsWith('Blazor-WebAssembly:');
Suggested change
return content.trim().startsWith('Blazor-Server-Component-State:') ||
content.trim().startsWith('Blazor-WebAssembly-Component-State:') ||
content.trim().startsWith('Blazor-Web-Initializers:') ||
content.trim().startsWith('Blazor-WebAssembly:');
const trimmedContent = content.trim();
return trimmedContent.startsWith('Blazor-Server-Component-State:') ||
trimmedContent.startsWith('Blazor-WebAssembly-Component-State:') ||
trimmedContent.startsWith('Blazor-Web-Initializers:') ||
trimmedContent.startsWith('Blazor-WebAssembly:');

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +15
export function isMetadataComment(node: Node): boolean {
if (node.nodeType !== Node.COMMENT_NODE) {
return false;
}
const content = node.textContent || '';
return content.trim().startsWith('Blazor-Server-Component-State:') ||
content.trim().startsWith('Blazor-WebAssembly-Component-State:') ||
content.trim().startsWith('Blazor-Web-Initializers:') ||
content.trim().startsWith('Blazor-WebAssembly:');
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isMetadataComment function lacks automated test coverage. Given that this function is critical for preventing metadata comments from entering the logical tree (which causes navigation errors), consider adding unit tests to verify:

  1. Correct identification of all metadata comment types (Blazor-Server-Component-State, Blazor-WebAssembly-Component-State, Blazor-Web-Initializers, Blazor-WebAssembly)
  2. Rejection of non-comment nodes
  3. Rejection of non-metadata comments (e.g., regular Blazor: component markers)
  4. Handling of whitespace variations in comment content

Example test structure:

describe('isMetadataComment', () => {
  test('should identify server component state comments', () => {
    const comment = document.createComment('Blazor-Server-Component-State:abc123');
    expect(isMetadataComment(comment)).toBe(true);
  });
  
  test('should not identify component marker comments', () => {
    const comment = document.createComment('Blazor:{"type":"server"}');
    expect(isMetadataComment(comment)).toBe(false);
  });
  // ... more tests
});

Copilot uses AI. Check for mistakes.
Comment on lines 111 to +116
element.childNodes.forEach(child => {
// Skip metadata comments that will be consumed during discovery
// These are not components and should not be part of the logical tree
if (isMetadataComment(child)) {
return;
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata comment filtering logic in toLogicalElement lacks automated test coverage. This is a critical fix for navigation errors, so consider adding tests to verify:

  1. Metadata comments are excluded from the logical tree during initial construction
  2. Non-metadata comments and other nodes are still included
  3. The logical tree structure remains correct after filtering

Example test:

test('should exclude metadata comments from logical tree', () => {
  const parent = document.createElement('div');
  parent.appendChild(document.createComment('Blazor-WebAssembly:{"options":"test"}'));
  parent.appendChild(document.createElement('span'));
  parent.appendChild(document.createComment('Regular comment'));
  
  const logicalElement = toLogicalElement(parent, true);
  const children = getLogicalChildrenArray(logicalElement);
  
  expect(children.length).toBe(2); // span and regular comment, but not metadata comment
});

Copilot uses AI. Check for mistakes.
Copy link
Member

@ilonatommy ilonatommy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to copilot's review: tests missing. Other than that, approved.

@ilonatommy ilonatommy added this to the .NET 11 Planning milestone Nov 26, 2025
@javiercn javiercn merged commit 3b674da into main Nov 26, 2025
37 checks passed
@javiercn javiercn deleted the javiercn/fix-enhanced-navigation-break branch November 26, 2025 08:35
@javiercn
Copy link
Member Author

javiercn commented Nov 26, 2025

+1 to copilot's review: tests missing. Other than that, approved.

I skipped the unit test because it doesn't prove anything. I considered adding an E2E test for this, but it will be complex to write. The issue is actually an edge case (it won't happen on all navigations, only when you have a component in a concrete DOM location)

I did manual validation with two independent repros to confirm it fixed the issue.

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

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.Net 10 Blazor Enhanced Navigation in WASM fails due to no PageTitle .NET 10 Blazor navigation from wasm to interactive server broken

3 participants