Repository navigation
Description
Describe the bug
Since 2.0.0-rc.19, when the SSR stream ends before a <Loading> fragment arrives, an async read created inside that boundary never runs on the client. Hydration finishes with no error and the boundary stays on its fallback. rc.18 renders the boundary on the client and fetches the data. It still reproduces on next (81a12d8).
Your Example Website or App
No public link. The snippet below is enough to reproduce it.
Steps to Reproduce the Bug or Issue
function Panel() {
const data = createMemo(() => fetch("/api/data").then(r => r.text()));
return <p>{data()}</p>;
}
export function App() {
return (
<Loading fallback={<p>loading</p>}>
<Panel />
</Loading>
);
}- Render with
renderToStream. - End the response right after the shell, before the fragment's chunk (for example, a proxy that closes the connection). The shell carries
_$HY.r["<id>_fr"]and the memo's own record, both pending. - Open the page and let the parser finish.
_$HY.done turns true without any error, but /api/data is never requested and loading stays. In our app, a page with two such boundaries makes 2 requests on rc.18 and 0 on rc.19.
Expected behavior
At DOMContentLoaded the boundary renders fresh, /api/data is requested once and the data shows, as on rc.18.
Screenshots or Videos
No response
Platform
- solid-js / @solidjs/web: 2.0.0-rc.19 and
next(81a12d8) - Browser: Chromium (Playwright)
Additional context
What I traced in packages/solid/src/client/hydration.ts:
- fix(solid): a rejected fragment's fresh render claims what the server sent #4319 (2c9fdd9) resumes a cut fragment with
0, andresumeBoundaryHydrationruns that fresh render with adoption on (servedPass(set, true), added in fix(solid): hydration never asks again for an answer the server already sent #4313 / e0e4224). The comment there says what the server never sent computes because "a truncated ref is dropped from the registry". The fix(solid): a rejected fragment's fresh render claims what the server sent #4319 changeset says the same: "Only reads with no record, such as those cut off by a truncated stream, compute." - That drop lives in
rejectTruncatedRefs, whichwatchTruncationschedules withsetTimeout. In the same handler,markTruncatedrejects the fragment's abort promise synchronously, so the resume with0and the fresh render run in the microtasks before the timer fires. - The memo in the fresh render still finds its pending record through
sharedConfig.has, adopts the promise viareadHydratedValue, and runs its compute only insidesubFetch, wherefetchandPromiseare mocked. When the timer later deletes the record, nothing rejects the adopted promise, so it never settles.
I have a fix with a test: drop those records at DOMContentLoaded, before marking the fragments truncated, and keep the deferred rejection for refs a keyed consumer already took. PR coming.
Activity
ryansolid commentedon Oct 29, 2026
Fixed on
nextby #4414 (5d57e12); it ships in the next RC.Generated by Claude Code