bake: retract a client component's bundling failure when it is demoted - #37862
bake: retract a client component's bundling failure when it is demoted#37862robobun wants to merge 4 commits into
Conversation
When a "use client" file stops being a client component boundary, the server graph deletes the client graph's node for it. If that node was failing, its SerializedFailure stayed in dev.bundling_failures and was never published as removed, so error overlays kept showing it and later "Build Failed" pages listed it again. Release the node's content and move its failure to incremental_result.failures_removed when the node is deleted.
|
Warning Review limit reached
Next review available in: 2 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 8:05 PM PT - Aug 12th, 2026
❌ @robobun, your commit 5024923 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 37862That installs a local version of the PR into your bun-37862 --bun |
|
Status: ready for review. Reproduced on the released build with the new tests in CI: across the last three builds every test lane that ran (including the x64 ASAN lanes, which run the bake tests) passed; the red is confined to build lanes that could not download the mimalloc / lolhtml / WebKit tarballs from github.com (and the jobs depending on them), plus already-flagged flaky tests unrelated to bake. I am not going to keep re-triggering; the diff itself is green. |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The fix reuses the same failure-removal sequence already present at three other sites in incremental_graph.rs, but since it touches the bake dev server's cross-graph state machine (tombstoned slots, unsafe sibling-field projection) a human look would still be worthwhile.
What was reviewed:
- Confirmed
disconnect_and_delete_filehas a single caller onclient_graph, so thedebug_assert!(SIDE == Client)infree_file_contentholds and the server graph is never affected. - Checked that
dev_bundling_failures()/dev_incremental_result()project to fields disjoint from both graphs and that the server-side caller holds no borrow on them at the call site. - Verified
FreeCssMode::UnrefCssmatches the mode used by the other node-goes-away paths (insert_stale_extra,insert_failure) rather than the replace path inreceive_chunk. - The test's synchronization relies on
dev.writeresolving after a later socket message, so error packets from the same rebuild are already recorded before the assertion; the harness'sonmessage→emit("hmr")ordering supports that.
Extended reasoning...
Overview
The PR adds ~20 lines to disconnect_and_delete_file in src/runtime/bake/dev_server/incremental_graph.rs so that when a client-graph node is tombstoned (only happens when a "use client" boundary is demoted from the server side), it releases the node's bundled content/source map and, if the node was failing, removes its entry from dev.bundling_failures and pushes it onto incremental_result.failures_removed — the exact sequence already used at three other places in the same file. A ~150-line test in test/bake/dev/bundle.test.ts subscribes the harness HMR socket to the errors topic, decodes the binary SerializedFailure / errors-packet wire format, and asserts the retraction is published and no longer appears on later "Build Failed" pages.
Security risks
None. This is dev-server-only error-tracking bookkeeping; no user input parsing, auth, crypto, or network-facing surface changes.
Level of scrutiny
Medium-high. The Rust change is small and mechanically mirrors an established pattern, but it lives inside the bake dev server's incremental graph, which coordinates two const-generic graph instances via unsafe owner()-based sibling-field projection and maintains subtle invariants about tombstoned slots (indices never reused, is_hmr_root intentionally preserved so a still-reachable tombstone keeps panicking rather than silently tracing). The PR description explains these invariants clearly and I verified the single caller and field-disjointness, but a maintainer familiar with bake should confirm the reasoning about slot state and the UnrefCss choice.
Other factors
The test is strong: it asserts the exact wire-format packet at each step (added → removed → later failure page lists only the new error), and the PR description confirms it fails on USE_SYSTEM_BUN=1 and passes on the debug/ASAN build along with the rest of bundle.test.ts (including the pre-existing demotion test that now also exercises the new code). No new unsafe blocks are introduced; the change only calls the pre-existing safe projection helpers. The core::mem::take / mutate / write-back pattern matches the one used in insert_stale_extra and insert_failure for the same borrow-checker reason. Given the subsystem's complexity I'm deferring rather than approving outright.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The retraction logic in disconnect_and_delete_file reuses the exact take/free_file_content/put-back and bundling_failures.fetch_swap_remove → failures_removed sequence already established in receive_chunk and insert_stale_extra, and the sole call site is on the client graph so the Side::Client assert in free_file_content holds. That said, this touches unsafe sibling-projected state in the incremental graph and adds ~240 lines of test infrastructure that hard-decodes the HMR errors wire format, so a human look is still worthwhile.
What was reviewed:
- Confirmed
disconnect_and_delete_filehas exactly one caller (client_graphfrom server-sidereceive_chunk), sofree_file_content's client-only assert and the newowner()-backed sibling accesses (bundling_failures,incremental_result,assets) stay disjoint from the server graph's live borrows. - Checked that
is_hmr_rootand other flags survive themem::take/write-back, matching the PR's stated intent for the tombstone. - Traced the three new tests against
serialized_failure.rs's layout and the harness'sdev.writesequencing; the overlay test usesexpectReloadrather than a sleep.
Extended reasoning...
Overview
The PR modifies disconnect_and_delete_file in src/runtime/bake/dev_server/incremental_graph.rs (+19 lines) so that when a client-component boundary is demoted and its client-graph node is deleted, the node's cached content/source-map are freed and any recorded bundling failure is removed from dev.bundling_failures and pushed onto incremental_result.failures_removed. It also adds ~240 lines to test/bake/dev/bundle.test.ts: two binary decoders for the SerializedFailure / MessageId.errors wire format, a recordErrorsPackets helper, and three devTests covering the working-component demotion, the failing-component demotion (packet-level), and the same demotion observed through the error overlay.
Security risks
None. This is dev-server bookkeeping for the HMR error overlay; no network input parsing, auth, or user-controlled data reaches the changed code.
Level of scrutiny
Medium-high. The Rust change is small and mechanically identical to existing patterns in the same file, but it lives inside the incremental graph's owner()-based unsafe sibling-projection scheme: free_file_content reaches (*dev).assets, and the failure retraction reaches (*dev).bundling_failures / (*dev).incremental_result, all while the server graph's receive_chunk already holds raw-pointer-derived borrows of client_graph and directory_watchers. I verified these are all disjoint DevServer fields, but this is exactly the class of code where a maintainer's eye is valuable. The test additions also hard-code the HMR errors wire format in the harness, which a bake maintainer should confirm they're comfortable maintaining.
Other factors
- The failure-removal block is byte-for-byte the same as the client and server arms of
receive_chunk, and the take/free/put-back matchesinsert_stale_extraandinsert_failure, so there's no new pattern to reason about. disconnect_and_delete_fileis private with a single call site onclient_graph, so thedebug_assert!(matches!(SIDE, Side::Client))insidefree_file_contentis satisfied and dead-code elimination handles the server monomorphization.- CI on the latest commit shows unrelated infra failures (aarch64 build-bun on macOS/FreeBSD, verify-baseline exit 56, an install-migration flake) with no
test/bakefailures, but the build is still in progress. - The comment-cop bot's note about a long comment was already addressed in eaba299.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The Rust change is small and mirrors the same cleanup sequence already used in receive_chunk and insert_stale_extra, but since it touches bake's incremental-graph node lifecycle and the tests hand-decode the HMR errors wire format, a human look would still be worthwhile.
What was reviewed:
disconnect_and_delete_file: verified the take →free_file_content→ write-back preservesis_hmr_rootand other flags, and theRawSlicekey stays valid until the later tombstone step.- Confirmed the only caller is
client_graph.disconnect_and_delete_file(...), so the client-onlydebug_assertinfree_file_contentholds. - Checked the failure-removal sequence matches the existing pattern in
receive_chunk(client) byte-for-byte. - Tests: the errors-packet decoder layout was cross-checked against the field order described for
SerializedFailure; the harness'semit("hmr", ...)anddev.socketare established patterns.
Extended reasoning...
Overview
The PR adds ~19 lines to IncrementalGraph::disconnect_and_delete_file in src/runtime/bake/dev_server/incremental_graph.rs so that when a client-graph node is tombstoned (a "use client" file loses its directive), its bundled content/source-map is freed and any recorded bundling failure is moved from dev.bundling_failures onto incremental_result.failures_removed. It also adds three devTest cases and two decoder helpers (~240 lines) to test/bake/dev/bundle.test.ts.
Security risks
None. This is dev-server error-overlay bookkeeping; no auth, no untrusted input parsing on the server side, no new unsafe blocks (the sibling-projection helpers dev_bundling_failures()/dev_incremental_result() are pre-existing).
Level of scrutiny
Medium-high. The Rust change itself is mechanical — it lifts the exact core::mem::take → free_file_content → fetch_swap_remove → failures_removed.push sequence already used in receive_chunk (client branch) and insert_stale_extra. But bake's incremental graph mixes const-generic side dispatch, RawSlice borrows of map keys, and container_of-style sibling access; a mistake here would be a UAF or stuck error state. I checked that free_file_content's debug_assert!(SIDE == Client) is satisfied (the sole caller is on client_graph), that the key RawSlice is not read after the slot's key is freed, and that non-content flags like is_hmr_root survive the take/write-back so the existing "missing component" panic still fires on a reachable tombstone.
Other factors
The tests are thorough (both HMR-socket-level and browser-overlay-level, plus the non-failing demotion under ASAN), and the PR description documents USE_SYSTEM_BUN=1 failing / debug+ASAN passing. The one bot comment (comment-cop about a long code comment) was addressed in eaba299. What gives me pause for auto-approval is that the tests introduce a hand-rolled decoder for the SerializedFailure / MessageId.errors binary format and a new "sre" subscription pattern — a maintainer should confirm they're happy taking that as test surface. That plus the general subtlety of the incremental-graph lifecycle keeps this out of the "obviously safe" bucket, so I'm deferring rather than approving.
Problem
serverComponents.separateSSRGraphon: a"use client"file breaks (Could not resolve: "./missing"), then one edit removes both the directive and the bad import. The route serves again, but no errors packet retracting the failure is sent, so browser overlays keep showing it.Fix
Server Incremental Graph is missing component for ""panic.Background
"use client"file is a boundary with a node in both graphs. Removing the directive (demotion) makes the server graph delete the client node; that node is an HMR root, so the edit never re-bundles it.separateSSRGraphis what makes the failure belong to the client node.Original description
Repro
Dev server with a framework that sets
serverComponents.separateSSRGraph = true(bun-framework-react does).routes/index.tsimportscomponents/Comp.ts, which starts out as a"use client"file and bundles fine. Then, in two edits:Edit 1 produces a
Could not resolve: "./missing"failure owned by the client graph's node forComp.ts, and an errors packet adding it is published on the HMR socket. After edit 2 the route serves fine again, but no errors packet removing the failure is ever published, so browsers keep the entry in the error overlay, anddev.bundling_failuresstill holds it for the rest of the process: the next unrelated failure (say inroutes/index.ts) renders a "Build Failed" page listing both the new error and the long fixedComp.tsone, and thebundling_failures.is_empty()checks infinalize_bundle(hot-update route list, "Reloaded in" line) and in the route state machine keep taking the failure path.Cause
Edit 2 re-bundles
Comp.tson the server only (the client node is an HMR root, soinvalidateskips it).server_graph.receive_chunksees a file that was a boundary and no longer is, and callsclient_graph.disconnect_and_delete_fileon the client node. That function disconnects the node's edges and tombstones its key, but leaves the node'sfailedentry indev.bundling_failuresand never pushes it toincremental_result.failures_removed. Every other place a node stops failing (receive_chunkon both sides,insert_failurereplacing an entry) does both; this is the one path where a node goes away without being received again, so nothing later can clean it up. It also left the node's last bundled code and source map behind, a small leak per demotion. The Zig original had the same omissions.Fix
disconnect_and_delete_filenow releases what the node owns before tombstoning it:free_file_content, and if the node was failing,fetch_swap_removeitsOwnerPacked(side, index)entry frombundling_failuresontofailures_removed, the same sequencereceive_chunkuses. The function only runs fromreceive_chunk, i.e. insidefinalize_bundlepass 1, andindex_failuresdrainsfailures_removedafter pass 2 of the same bundle, so the removal is published in the same errors packet the bundle would send anyway, keyed by the owner id the client already has (tombstoned indices are never reused, so the id stays unambiguous).The rest of the tombstoned
Fileis intentionally left as it was. In particular it keepsis_hmr_root, so a tombstone that is still reachable keeps hitting the existingServer Incremental Graph is missing component for ""panic instead of being silently traced as a dead end; this change is only about the failure and content the slot owned.Verification
Three tests added to
test/bake/dev/bundle.test.ts:components/Comp.ts, edit 2 publishes exactly{ removed: [that owner], added: [] }and the route then serves the demoted module, and a later unrelated failure's "Build Failed" page lists only that failure. On the released build the second step receives no packet at all and the page lists both failures.Comp.tserror and no reload happens.Runs:
USE_SYSTEM_BUN=1): the first two fail as described, the third passes"use client"file that never bundled take this same demotion path: demoting such a file publishes{ removed, added: [] }and later error pages no longer list it, so the two changes compose (on main alone that scenario never reachesdisconnect_and_delete_file, and this PR leaves it unchanged)[review] gate passed · iteration 1 · 2 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file