You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
renderToStream hangs when a nested <Loading> settles before its parent fragment fails #3478
renderToStream never ends when a nested <Loading> settles before a sibling async read in the same parent fragment rejects. The outer fragment's failure abandons its subtree (#3165), but a nested fragment that has already settled — its content template and $df emitted — leaves something the response's completion still waits on. Reproduced against next (3ae9e921b) with the web and solid server entries from source.
Reproduction
functionFast(){constfast=createMemo(async()=>{awaitdelay(5);return"early";});return<span>{fast()}</span>;}functionChild(){constbad=createMemo(async()=>{awaitdelay(60);thrownewError("boom");});return(<div><Loadingfallback={<i>inner</i>}><Fast/></Loading>{bad()}</div>);}// never resolves — `end()` is never called on the sinkawaitnewPromise(resolve=>renderToStream(()=>(<Loadingfallback={<i>outer</i>}><Child/></Loading>)).pipe({write(){},end: resolve}));
Two variations that do complete, which bound the bug:
the nested boundary's read rejects/resolves after the sibling's failure (Fast at 60 ms, bad at 5 ms);
So the case that hangs is specifically: inner fragment settled first, then the outer fragment that contains it fails. Argument order matters — the nested <Loading> must be created before the failing read throws NotReadyError on the first pass (here it precedes {bad()} in the template), which is an ordinary app shape.
Where to look
renderToStream's fragment registry and abandonSubtree (packages/web/src/server.ts): abandonment walks registry for keys under the failed one and pendingSerialized for ids under it. A nested fragment that already settled has left the registry; whatever it still holds open — its reveal-group registration, a blockingPromises/flushEnd gate, its _fr stub batched pre-shell — is not in either table when the parent fails. SSR_RENDER_ERROR_CONTAINED (handling: "client") is recorded for the outer fragment, so the failure itself is seen; it is the completion accounting that is wrong.
Found while writing the tier fixture for #3477; not fixed there. Refs #3165.
Root cause: when a nested <Loading> settled while its parent fragment was still pending, its markup was parked on the parent's registry entry to be spliced in on resolve. When the parent then failed, the splice ran over an undefined value, replacePlaceholder threw out of the fragment resolver before item.resolve(error) could reject <key>_fr, and the response waited on it forever — which is also why no SSR_SUBTREE_ABANDONED finding appeared and the boundary recorded a second SSR_RENDER_ERROR_CONTAINED. The error path now drops the parked children; the client re-renders the whole subtree off the outer rejection.
Activity
fix(web): a failing fragment drops its settled nested fragments inste…
Merge pull request #3480 from brenelz/investigate/stream-hang-nested-…
ryansolid commentedon Sep 15, 2026