Skip to content

fix: resolve nested defer() payload references in production builds#151

Merged
uhyo merged 1 commit into
masterfrom
claude/framework-audit-l7vkod
Jul 19, 2026
Merged

fix: resolve nested defer() payload references in production builds#151
uhyo merged 1 commit into
masterfrom
claude/framework-audit-l7vkod

Conversation

@uhyo

@uhyo uhyo commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Problem

Nested defer() — a deferred server component calling defer() again — produced broken production builds, in two independent ways:

  1. Wrong ID-finalization order. topologicalSort returned dependents before dependencies, so processRscComponents content-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>.txt and 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).

  2. Nested registrations missed entirely (non-SSR builds). DeferRegistry.loadAll snapshotted the registry once, but a nested defer() 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 the topologicalSort unit tests encoded the reversed order.

Changes

  • build/dependencyGraph.ts: topologicalSort now 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: loadAll keeps 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

  • New build/rscProcessor.test.ts: nested chain, diamond references, hash determinism across different temp IDs/registration orders, and cycle handling.
  • build/dependencyGraph.test.ts updated to the corrected order contract.
  • E2E: both fixtures now use nested defer()fixture-ssr-defer (SSR build path, covers the sort fix) and fixture (non-SSR path, covers the loadAll fix) — 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

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
@uhyo
uhyo merged commit 16be30d into master Jul 19, 2026
2 checks passed
@uhyo
uhyo deleted the claude/framework-audit-l7vkod branch July 19, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants