Repository navigation
fix(web): escape < in useHead JSON script bodies - #4012
Open
everton-dgn wants to merge 2 commits into
Open
everton-dgn wants to merge 2 commits into
everton-dgn wants to merge 2 commits into
Conversation
useHead only rewrote `</script` in inline script bodies on the server. In a JSON-LD script, a value such as `<!--<script>` moves the HTML tokenizer into the double-escaped state, so the closing `</script>` no longer ends the element and the markup after it becomes script text, up to a later `</script>` or the end of the document. Scripts whose body is JSON (importmap, speculationrules, and the JSON MIME types: application/json, text/json, any +json subtype) now get every `<` written as `\u003C`, which parses back to the same value. The shell markup, the streamed head ops and the client registry share the helper, so the client writes and compares the same text and hydration adopts the server element instead of replacing it. Other scripts and data blocks keep their current body. On the server the escape is installed by useHead, the only way tags reach the head registry, so a render without useHead (the server-render size floor) does not carry it.
🦋 Changeset detectedLatest commit: bd1f661 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will not alter performance
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
useHeadonly rewrote</scriptin inline script bodies on the server. In a JSON-LD script, a value like<!--<script>moves the HTML tokenizer into the double-escaped state, so the closing</script>no longer ends the element and the markup after it becomes script text, up to a later</script>or the end of the document (in the issue's repro, the whole body).Scripts whose body is JSON now get every
<written as\u003C, which parses back to the same value. That coversimportmapandspeculationrules, matched the way HTML matches script types, and the JSON MIME types from MIME Sniffing (application/json,text/json, any+jsonsubtype). Other data blocks keep their body. The helper,headBodyTextinhead.ts, runs wherever the registry produces or compares a body:renderHeadTagMarkup(shell and resource markup) andflushHeadFragment(streamed["a", ...]ops the client applies withtextContent), through a hook the serveruseHeadinstallscreateHeadElementandheadElementMatcheson the clientSince the client writes and compares the escaped text, hydration claims the server element. Before, a JSON body with
</scriptwent out as<\/script, never matchedchildren, and the client swapped the element.Classic and module scripts keep the existing
</scriptrewrite, so a classic script with</scriptin its body still doesn't match on hydration and is still replaced; that needs its own fix. Client-created JSON scripts also hold the escaped text now:JSON.parse(el.textContent)returns the same value, but code reading the raw string would see\u003C.Public API changes
None. No export, prop, option or type changes;
headBodyTextlives in the internalhead.tsand is not re-exported. The only observable change is the text described above: JSON script bodies rendered by the server, and thetextContentof JSON scripts the client creates.Size
useHeadis the only way tags reach the server head registry, so the escape is installed byuseHeadand a render that never imports it doesn't carry it. Againstnext@ 84dada7 every size scenario measures 0 B, theserver: renderToStringfloor included (20,403 B brotli, 71,817 B minified), and no cap moves. Only bundles that importuseHeadpay for the escape.How did you test this change?
New tests in
packages/web:test/server/use-head-json-script.spec.tsxrenders<!--<script>in JSON-LD and parses the output with jsdom to check that the<main>after it survives. It pins the escaped markup for each JSON type (includingImPoRtMaPandApplication/LD+JSON; charset=utf-8), checks that other data blocks keep their body (video/json,custom+json,importmap;x,importmap,application/jsonnext to a form feed,text/plain, a Symboltypeand a few more), checks that classic and module scripts are unchanged, and checks the body carried by a streamed patch op.test/runtime/head-json-script.spec.jsseeds server-shaped markup and checks that the client claims the same element, that a JSON script the client creates carries the escaped text, and that other data blocks, a Symboltypeincluded, keep their text.Without the fix, 11 of the 22 server tests and 2 of the 3 client tests fail; the ones that pass either way are the guards for classic scripts and non-JSON data blocks. With it, all pass.
Closes #4011