Repository navigation
fix(solid): an Errored fallback's own failure goes to the next boundary up; its wait is budgeted (#4316, #4317) - #4318
Conversation
…ry up; its wait is budgeted (#4316, #4317) #4316: when a <Loading> fails after the shell and its <Errored> renders the fallback as the outcome (renderOutcome), a fallback hole that rejected while the server waited for it was swallowed. The retry ran with no owner, so the hole's error never met a handler and the outcome came back empty. Parked in a pending outer <Loading>, the inner fragment then settled as a blank child, and the client hydrated the blank and halted. A fallback that threw synchronously reached the parent handler instead, but in its no-consumer mode: an outer <Errored> serialized its error record and rendered a fallback nobody sent. Now a failure of the fallback, sync or async, goes to the next boundary up. The fallback owner's handler holds it while the outcome renders, and the retry runs under that owner. The parent is first asked for the outcome at the same position, so an outer <Errored> whose whole content is that position draws its fallback there and serializes its error with it. Otherwise the failure is routed as a content failure, so a pending outer <Loading> fails its fragment. An outer <Errored> reached that way while it is not being pulled lets it pass and does not render. #4317: the wait now shares the <Loading> resume loop's convergence budget (MAX_RESUME_PASSES, internal; hydration.ts uses it in place of the 10000 literal). Past the budget it answers what is available, which is no finished fallback. It also races each wait against the fallback owner's teardown, so an aborted render stops waiting at once. Tests: sync-throw twins for the parked and unparked shapes, with and without an outer <Errored>. The server harness holds the async variant's markup to the twin's. The hydrate spec holds its client outcome, loaded and streamed, to the twin's, and a caught variant must claim the outer fallback. Server specs cover the budget (a fresh pending source every pass) and the abort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 8692ad9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 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 |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 40460326369Coverage remained the same at 78.589%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 23.9%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
…s to the nearest boundary (#4316) A <Loading> that failed after the shell while its fragment was parked in an enclosing <Loading> still pending was settled as a blank region that reported success. The renderer (web/src/server.ts registerFragment) parks a settled child into its pending parent and resolves its `_fr` whether or not it failed. Nothing upstream heard of the failure: the error was routed only to an <Errored> that could draw at exactly that position (errorOutcome). The client then hydrated the blank region as success and halted or drew nothing. Now the failure is the enclosing fragment's content's, as when the same read fails in that fragment's own render pass. Handlers get an escalation mode (ESCALATE, server-internal) for a failure that surfaced after the render passed them: - A <Loading> whose fragment is still pending fails it, the way a hole failure in its own pass does. - An <Errored> whose finished markup sits raw in such a fragment renders its fallback in place of that markup (FragmentContainer.region): the fallback is waited for, its error is serialized once it is finished, and its own failure goes up the same way. An <Errored> still pending shows the fallback on its next pull, and one already showing its fallback absorbs the failure. - A boundary already settled with a failure absorbs it. - Otherwise the failure goes to the next boundary up. The enclosing <Loading> applies the regions to its markup before it settles, outermost first. If a region is not found, or its fallback never finishes, that fragment fails with the region's error. This replaces the `escalating` counter from 7ce3c98: an <Errored> fallback that fails while the server waits for it uses the same mode, so it is drawn one level up too. Not covered: - The late case. If the enclosing fragment settled before the inner read failed, its markup is already out. The inner fragment rejects on its own and the client renders it, and the client's <Errored> meets the failure, unchanged. - An <Errored> inside a nested fragment that settled into the pending one first. Its markup is out of the enclosing fragment's own markup, so the enclosing fragment fails instead of drawing. Tests: parked shapes with an <Errored> between, around the inner <Loading> with a failing fallback, and around everything, each against a twin with no inner <Loading> (the failure met in the enclosing pass). The server harness holds the stream text to the twin's. The hydrate spec, loaded and streamed, holds the client outcome to the twin's and checks that the drawn fallback is claimed. Server specs cover the late case and a nested fragment that settled first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…osing boundary An `<Errored>` whose fallback failed while rendering (a hole that rejects, or a synchronous throw) for its content's error was left blank on the server: the boundary's error handler had already marked the error handled before rendering the fallback, so the fallback's own failure was dropped and the pull returned `undefined`. At shell level, with no `<Loading>`, the shell got a blank region with the errors recorded and no markup for them; an enclosing `<Errored>` serialized its record but never drew. The fallback's failure now passes up from the pull: an enclosing `<Errored>` draws its fallback in place of the region, and with none the request fails, the same for the async hole and its sync-throw twin. Server only; client bundles are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Keeps both sides of the harness scenarios and the 4306 spec imports. #4318's new artifacts and its rejected-fragment expectations take this branch's empty outcome template (`e></template>`, no lone space). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
Summary
Fixes #4316. Fixes #4317. Server only. Client bundles are byte-identical to next (0 B in all 22 size scenarios).
#4316, the fallback's own failure (7ce3c98).
Before: when an
<Errored>fallback's own async hole rejected (the #4306 nested shape), #4308's wait loop re-rendered the holes with no owner.ssrHandleErrorthrew raw andinCtx's emptycatch {}swallowed it. The fragment rejected and parked as a blank child of the pending outer<Loading>. The server sent<b>outer</b>with a blank region, and the client logged key misses and then hit REACTIVITY_HALTED.After: the failure goes to the next boundary up, the same way a fallback that throws synchronously does. An enclosing
<Errored>that can draw at that position answers with its own fallback. Otherwise the error fails the enclosing content, so a pending<Loading>fails its fragment.This also fixes the sync twin in the caught shape. An outer
<Errored>reached while it wasn't rendering used to serialize a_$HY.rrecord for a fallback whose markup was never sent.#4316, parked fragments (f14ce8d).
Before: any fragment that failed while parked in a pending parent fragment went out blank.
registerFragment's parked branch (web/src/server.ts) storeschildren[key] = value ?? ""and resolves with no error.failFragmentonly asked an<Errored>that could draw at exactly that placeholder, which is never true with markup in between (for example<Loading><Errored><section><Loading>…).After:
failFragmentsettles the parked fragment, then hands the failure up with an internalESCALATEhandler argument. This replaces 7ce3c98'sescalatingcounter, so a failing fallback uses the same path.<Loading>receiving the failure fails its own fragment.<Errored>whose finished markup sits raw in a pending fragment draws its fallback in place of that markup. The fallback is waited for, and its error is serialized only once it is finished. The pending<Loading>applies those regions before it settles, or fails rather than going out blank.<Errored>that is already showing its fallback, or has already settled with a failure, absorbs it.ESCALATEis not exported from any package entry.Shell-level fallback failure (8692ad9). This is the same bug family, found while testing the above.
Before: an
<Errored>whose fallback failed while rendering, outside any<Loading>, rendered blank. The boundary's handler marked the error handled before rendering the fallback, and the catch'shandled ? result : …then returnedundefined. That dropped the fallback's failure. An enclosing<Errored>serialized its record but never drew. The shell got a blank region with_$HY.rrecords and no markup for them.After: the fallback's failure passes up from the pull. An enclosing
<Errored>draws its fallback in place of the region. With none, the request fails, the same way for an async hole and for its sync-throw twin.#4317. Before:
renderOutcome's wait had no pass limit and didn't stop when the render was torn down.After: it counts passes against
MAX_RESUME_PASSES, the same 10000 budget the<Loading>resume loop uses.MAX_RESUME_PASSESis@internalin server/signals.ts and not re-exported. Past the budget it answers what is available, which is nothing for an unfinished fallback, so the fragment rejects. Each await is raced against the fallback owner's cleanup.Known limits:
<Errored>catches it.<Errored>inside a nested<Loading>that already settled into the pending outer one: the outer fragment fails rather than drawing. It never produces a blank success.<Errored>with an async fallback: it can lose to the outer<Loading>settling first. The sync twin has the same race.<Errored>around a pending read in a pending<Loading>: this is client side, and fix(solid): a rejected fragment's fresh render claims what the server sent #4319 fixes it.How did you test this change?
scenarios.tsxgains three families, each run against a sync twin:fallbackHoleFailure: 8 variants.parkedFailure: 8 shapes plus 2 late variants.shellFallbackFailure: 8 variants.hydration-harness.spec.tsxasserts that each variant's server output equals its twin's. It also checks that there are no blank<section></section>regions, and that uncaught variants report the error throughonError.errored-parked-async-outcome-4306.spec.tsxruns every variant in loaded and streamed mode. Each must equal its twin in server DOM, final text, halted, done, errors and warnings. Caught variants show their fallback without halting or warnings, and claim the server's nodes.errored-fallback-wait-4317.spec.tsx: the pass budget and abort.parked-fragment-failure-4316.spec.tsx: a nested<Loading>settled first.server-functions-adapter-request, fails identically on next in this container.tscclean.🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2