fix: resolve nested defer() payload references in production builds#151
Merged
Conversation
Nested defer() (a deferred server component calling defer() again) was broken in production builds in two ways: - topologicalSort returned dependents before dependencies, so a parent payload was content-hashed and written while still containing the child's temporary UUID, while the child was written under its hashed name. The client's fetch of the referenced payload then 404'd at runtime, and the parent's hash was nondeterministic across builds. The sort now returns dependency order, so every referenced payload's final ID is known before the referencing payload is hashed. - DeferRegistry.loadAll snapshotted the registry once, but in non-SSR builds a nested defer() registers its child while the parent's stream is draining, after the snapshot — so the child payload was never written at all. loadAll now keeps picking up entries registered mid-iteration until none are left. Adds unit tests for processRscComponents covering nested and diamond references, hash determinism, and cycles, and extends the e2e fixtures (SSR and non-SSR) with nested defer() usage plus assertions that no payload request fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JZ7N3pgH9q6i9qKpUcrwn
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.
Problem
Nested
defer()— a deferred server component callingdefer()again — produced broken production builds, in two independent ways:Wrong ID-finalization order.
topologicalSortreturned dependents before dependencies, soprocessRscComponentscontent-hashed and froze a parent payload while it still contained the child's temporary UUID, while the child's file was written under its hashed name. At runtime the client fetches/funstack__/fun__rsc-payload/<uuid>.txtand gets a 404 — which takes down the entire client render, not just the deferred subtree. It also made the parent's hash nondeterministic across builds (it hashed a random UUID).Nested registrations missed entirely (non-SSR builds).
DeferRegistry.loadAllsnapshotted the registry once, but a nesteddefer()registers its child while the parent's stream is draining — after the snapshot. The child payload was never rendered or written at all.Neither path was covered by tests: e2e only exercised single-level
defer(), and thetopologicalSortunit tests encoded the reversed order.Changes
build/dependencyGraph.ts:topologicalSortnow returns dependency order (every node appears after all nodes it references), so every referenced payload's final hashed ID is known before the referencing payload is hashed. Cycle handling is unchanged.rsc/defer.tsx:loadAllkeeps picking up entries registered mid-iteration until none are left. Termination is safe because any nested registration happens before its parent's drain promise resolves — once no drains are pending, no new entries can appear.build/rscProcessor.ts: comment updated to state the ordering invariant.Tests
build/rscProcessor.test.ts: nested chain, diamond references, hash determinism across different temp IDs/registration orders, and cycle handling.build/dependencyGraph.test.tsupdated to the corrected order contract.defer()—fixture-ssr-defer(SSR build path, covers the sort fix) andfixture(non-SSR path, covers theloadAllfix) — with assertions that the nested content renders and that no/funstack__/payload request fails.Verified: 77 unit tests, all 29 e2e tests, typecheck, lint, and format pass. Reverting only the two source fixes makes 7 e2e tests fail, confirming the new tests are real regression coverage.
🤖 Generated with Claude Code
https://claude.ai/code/session_011JZ7N3pgH9q6i9qKpUcrwn
Generated by Claude Code