Skip to content

Treat marker-less HTML starting with a doctype or <html> as a document - #11877

Merged
brophdawg11 merged 2 commits into
remix-run:mainfrom
devtechedge:devtechedge/flush-kind-inference
Sep 22, 2026
Merged

brophdawg11 merged 2 commits into
remix-run:mainfrom
devtechedge:devtechedge/flush-kind-inference

Conversation

@devtechedge

Copy link
Copy Markdown

When a frame receives HTML with no flush marker, the flush kind is now inferred from the markup instead of defaulting to fragment. HTML that starts with a doctype or <html> is treated as a whole document.

Why

renderToString strips flush markers before returning, so its output carries no <!-- rmx:flush document --> marker. createFrame only takes the full-document-reload path when flushKind === 'document', and renderFrameStream applied its end-of-stream remainder as a fragment. Marker-less document HTML therefore went through createFragmentFromString, which parses inside a <template> and drops the <html>/<head>/<body> wrappers. diffNodes then tried to insert those nodes beside documentElement and Chromium threw:

HierarchyRequestError: Failed to execute 'insertBefore' on 'Node': Only one element on document allowed.

The Navigation API keeps the committed URL and fires navigateerror, so a click looked like a no-op: the URL changed, the document did not, and nothing appeared in the console.

Inferring the kind also covers static output that lost its comments to a minifier, and restores the document sniffing finalizeHtml did before #11353.

Reproduction

https://github.com/devtechedge/remix-render-to-string-navigation

A minimal app with a client entry on page A and two destination pages, one rendered with renderToStream and one with renderToString. npm run verify clicks both links in Chromium and prints the URL, title, and heading before and after each click. Before this change only the renderToStream link navigates; after it both do.

Tests

Three regressions in packages/ui/src/test/frame.test.ts cover the stream path, the string path, and real renderToString output. All three fail on main with the HierarchyRequestError above and pass with this change.

The full packages/ui suite is green: 1081 tests, 1056 pass, 0 fail, 24 todo.

Fixes #11808

devtechedge added a commit to devtechedge/oss-contributions that referenced this pull request Sep 15, 2026
`renderToString` strips flush markers before returning, so its output carries
no `<!-- rmx:flush document -->` marker and the client runtime never takes the
full-document-reload path. The response is diffed as a fragment instead, which
parses it inside a `<template>`, drops the `<html>`/`<head>`/`<body>` wrappers,
and throws HierarchyRequestError while diffing against `documentElement`.

When a frame receives HTML with no flush marker, infer the kind from the markup
instead of defaulting to `fragment`: HTML starting with a doctype or `<html>` is
a whole document. This also covers static output that lost its comments to a
minifier.

Fixes #11808
@brophdawg11

Copy link
Copy Markdown
Contributor

/review

@brophdawg11 brophdawg11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

@brophdawg11
brophdawg11 merged commit 0bb8ed1 into remix-run:main Sep 22, 2026
18 checks passed
@devtechedge

Copy link
Copy Markdown
Author

Matt, thanks for reviewing and merging this one.

Seeing Aron independently reproduce it and land on the same marker-less inference approach gave me a lot of confidence the fix was shaped right.

The fragment-versus-document classification was the real lesson here, and working through it with this codebase was a ton of fun. 🚀

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

renderToString strips the flush marker, so its output cannot be navigated by the client runtime

2 participants