Skip to content

Shell-end marker is written after </html> in fully assembled documents (rc.15, #4002) #4030

Closed

Description

@ryansolid

What happens

In @solidjs/web 2.0.0-rc.15, a render that outputs <HydrationScript /> ends its shell with the shell-end marker from #4002. When the shell is the whole document (async render mode, i.e. the sync/complete assembly path, or a streamed shell with nothing pending), the marker is appended after </html>:

…<p _hk=140a000 id="streamed">STREAMED-ASYNC-CONTENT</p><!--/--></main></body></html><script>_$HY.s[""]=1,_$HY.sw&&_$HY.sw("")</script>

Browsers still run the script, because the parser reprocesses it into <body>. But content after </html> is a parse error, and the document no longer ends with </html>.

Where

packages/web/src/server.ts, both call sites of shellEndMarker:

  • the complete-document path (assembleDocument(...) + shellEndMarker(...), around L1902)
  • the stream sink's shell() (assembleDocument(...) + shellEndMarker(...), around L2439)

Both add the marker to the end of the assembled string instead of splicing it into the document.

Expected

The marker should still run after every shell node, but inside the document: spliced in right before </body> when the assembled shell has one, and appended as now otherwise.

Downstream impact

@solidjs/vite-plugin's start-ssr suite checks that async-mode documents arrive as "doctype … </html> in one piece". Against rc.15, 7 of its 749 assertions fail (render-mode override, async dev and prod, per-request header mode, authored entries). Plugin CI will go red once its lockfile moves to rc.15. Client mode had a separate rc.15 problem, which is fixed plugin-side in solidjs/solid-vite-plugin#413.

Repro

In the examples/start-ssr folder of solid-vite-plugin next, with solid-js and @solidjs/web at 2.0.0-rc.15 (or packed from solid next at eae6e03), run SSR_RENDER_MODE=async vite. Then curl -H 'accept: text/html' localhost:5173/ | tail -c 200 shows the marker after </html>.

Activity

  1. ryansolid commented on Oct 13, 2026

    @ryansolid
    MemberAuthor

    Fixed in #4034 (merged into next as c032934). The shell-end marker is now spliced in right before </body>, so a whole document ends with </html> again. That covers async render mode and a stream with nothing pending. A shell without </body>, such as an embedded render, still gets the marker appended. The renderToString floor was raised to 20.51 KB for it.

    — Claude via Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions