Skip to content

bake: serialize bundler messages with line 0 as having no location - #37885

Closed
robobun wants to merge 1 commit into
mainfrom
farm/2020bea6/bake-overlay-line-zero-location
Closed

robobun wants to merge 1 commit into
mainfrom
farm/2020bea6/bake-overlay-line-zero-location

Conversation

@robobun

@robobun robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In a Bake dev server, a plugin that throws from onLoad or onResolve makes the error overlay show an extra empty file group per failure; with notes or several messages per file the payload decodes as garbage.
  • The extra entry is keyed by owner 0, which the server never removes, so the error page does not reload once the real error is fixed.
  • Cause: a bundler message with line == 0 was written with column, length and line text, but the client stops reading after a zero line, so the leftover bytes decode as further failures.
  • Plugin exceptions are the common source of such messages; Location::default() and native plugin logs produce them too.

Fix

  • The writer now treats line <= 0 (previously only line < 0) as "no location" and writes a lone 0.
  • The client reader already returns null for line == 0 and bun_ast::Location documents line <= 0 as "no position", so the writer was the one side out of contract.
  • Not changed: the terminal still prints at file.ts:0, and native plugin logs with line > 0 but column <= 0 still break; tracked separately.
  • Verification: three new dev server tests (onLoad throw, onResolve throw, error page reloads after a fix) fail on 1.4.0 with USE_SYSTEM_BUN=1 and pass with this change. Existing tests for real locations and line: -1 still pass.

Background

  • Bake is bun's dev server for HTML and framework routes. On a failed bundle it serves a 500 "Build Failed" page with the failures embedded, and pushes later ones to the overlay over the HMR socket.
  • That payload is a hand-written binary format with no field tags: each write function in the Rust serializer has a matching read function in the client, so a field written but not read shifts every later byte.
  • Each failure is keyed by an owner, an index into the dev server's incremental graph; the server removes failures by owner as files rebuild, and the page reloads once none are left.
  • bun_ast::Location lines are 1-based; line <= 0 means no position. Plugin exceptions yield line: 0, resolution errors line: -1.
Original description

What

In a Bake dev server (bun ./index.html, Bun.serve({ development: true })), a bundler message whose Location.line == 0 was serialized with more fields than the error overlay reads, so the overlay decoded the rest of the message as further failures.

Plugin exceptions are the common way to get such a message: bun_ast_jsc::msg_from_js (used for onLoad / onResolve throws) builds a Location with line: 0, as does Location::default(), and the bun-native-plugin crate fills BunLogOptions with line: 0 for every log.

Repro: a plugin that throws from onLoad or onResolve while serving an HTML route or a framework route.

build.onResolve({ filter: /^trigger$/ }, () => {
  throw new Error("onResolve boom");
});

Decoding the payload embedded in the 500 page (or sent as BunFrontendDevServer.bundleFailed / over the HMR socket) with the real client decoder yields, on 1.4.0:

[
  { file: "index.ts", messages: [{ message: "onResolve boom", location: null, notes: [] }] },
  { file: null, messages: [] },   // leftover column / length / lineText bytes
]

User visible effects:

  • the overlay shows an extra empty file group per failure (with notes or more than one message per file, the rest of the payload is garbage and decoding throws);
  • the bogus entry is keyed by owner 0, which the server never removes, so buildErrors never empties and the error page does not reload after the error is fixed. The new "error page reloads once a plugin exception is fixed" test fails on 1.4.0 with location.reload() was not called even though the server logged Reloaded in 2ms: index.ts.

Why

write_log_data in serialized_failure.rs short-circuited only on line < 0. readBundlerMessageLocationOrNull in client/error-serialization.ts returns null for line == 0 after reading just the line, and bun_ast::Location documents line <= 0 as "no line and column information" (it is 1-based). The writer is the side that disagrees with both the reader and the documented contract, so it now writes a lone 0 for any line <= 0. Fixing the producers instead (e.g. making msg_from_js use -1) would change the public BuildMessage.position of Bun.build and would still leave Location::default() and native plugin logs broken, and the wire format has no way to express a line of 0 anyway.

Left alone on purpose: the terminal printer prints at file.ts:0 for these messages (cosmetic, unrelated to the wire format), and native plugin logs with line > 0 but column <= 0 still panic the writer / throw in the overlay; that is a different defect and is tracked separately.

Tests

test/bake/dev/plugins.test.ts:

  • exceptions thrown from onLoad are reported without a location: two failing files, decodes the error page payload with decodeSerializedError from the client and asserts exactly those two failures and nothing else in the payload;
  • exception thrown from onResolve is reported without a location: same for the resolve path (the failure belongs to the importer);
  • error page reloads once a plugin exception is fixed: [serve.static] plugin with an HTML route, loads the error page in the harness client, fixes the import and asserts the page reloads.

All three fail with USE_SYSTEM_BUN=1 (the first two decode an extra { file: null, messages: [] } per failure, the third times out waiting for the reload) and pass with bun bd test. test/cli/inspect/BunFrontendDevServer.test.ts and test/bake/dev/bundle.test.ts (messages with real locations, and resolution errors with line: -1) still pass.

write_log_data only treated a negative line as "no location" and wrote the
column, length and line text for a location with line == 0, while the
client reader (readBundlerMessageLocationOrNull) stops after a zero line.
Messages created from plugin exceptions (bun_ast_jsc::msg_from_js) and
native plugin logs carry line 0, so the overlay decoded the leftover bytes
as an extra empty failure that the server could never remove, which also
kept the error page from reloading once the real error was fixed.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75adaf6d-936f-4de1-9608-b54864eeccf9

📥 Commits

Reviewing files that changed from the base of the PR and between 9a543cc and a52a40a.

📒 Files selected for processing (2)
  • src/runtime/bake/dev_server/serialized_failure.rs
  • test/bake/dev/plugins.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:05 PM PT - Aug 12th, 2026

❌ @robobun, your commit a52a40a has 2 failures in Build #93385 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37885

That installs a local version of the PR into your bun-37885 executable, so you can run:

bun-37885 --bun

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed, waiting on CI.

Reproduced on 1.4.0 by decoding the payload of the dev server's 500 page with the client decoder (decodeSerializedError) for a plugin that throws from onLoad / onResolve: each failure is followed by a bogus { file: null, messages: [] } entry, and the error page never reloads after the import is fixed (location.reload() was not called). The three new tests in test/bake/dev/plugins.test.ts fail on 1.4.0 and pass with this branch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-character fix (< → <=) aligning the writer with the reader and the documented bun_ast::Location contract.

What was reviewed:

  • Confirmed readBundlerMessageLocationOrNull stops after reading a zero line and bun_ast::Location documents line <= 0 as "no position", so the writer was the mismatched side.
  • Checked that line < 0 (existing resolution errors) and line > 0 paths are unchanged; only the previously-broken line == 0 case is affected.
  • Tests use the real client decoder via the existing bake/* path alias and cover onLoad, onResolve, and the end-to-end reload behavior; harness conventions (devTest, await using, expectReload) match neighboring tests.
Extended reasoning...

Overview

The PR changes one comparison in src/runtime/bake/dev_server/serialized_failure.rs (loc.line < 0 → loc.line <= 0) so that a bundler message with line == 0 is serialized as "no location" (a lone zero) rather than as a full location record. It also updates a stale doc comment pointing at the reader file. Three new tests in test/bake/dev/plugins.test.ts cover onLoad throws, onResolve throws, and the end-to-end error-page-reloads-after-fix behavior.

Correctness

I verified the fix against both sides of the contract: readBundlerMessageLocationOrNull in src/runtime/bake/client/error-serialization.ts:69-70 reads a u32 and returns null immediately when it is zero, and bun_ast::Location at src/ast/lib.rs:636-637 documents "Line <= 0 means there is no line and column information". The writer was the only party that disagreed. The change is scoped to exactly the line == 0 case, which was previously producing a desynced byte stream; line < 0 and line > 0 behavior is unchanged. The PR description's rationale for fixing the writer rather than the producers (would change public BuildMessage.position, wouldn't cover Location::default() / native plugins, and the wire format can't express line 0 anyway) is sound.

Security risks

None. This is the dev-server error overlay wire format between Bun's own writer and its own HMR client, in development mode only. No untrusted input, auth, crypto, or external surface is involved.

Level of scrutiny

Low-to-medium. The runtime change is a single comparison operator in a self-contained serialization helper with a matching reader in the same codebase. It is not on a hot path, not memory-sensitive, and the failure mode it fixes was already user-visibly broken. No CODEOWNERS cover src/runtime/bake/ or test/bake/.

Other factors

The tests are well-constructed: they import the real decodeSerializedError / DataViewReader via the existing bake/* tsconfig path alias (already used by test/cli/inspect/BunFrontendDevServer.test.ts), so a future writer/reader mismatch fails here too. The two-failing-files onLoad test and the sort-by-file guard against ordering nondeterminism. The reload test uses await using, expectReload, and expectMessage from the bake harness rather than sleeps. The PR description states all three fail under USE_SYSTEM_BUN=1 and that BunFrontendDevServer.test.ts / bundle.test.ts (real locations, line: -1) still pass. No outstanding reviewer comments on the timeline.

@robobun

robobun commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #39488, which consolidates the open Bake PRs from this period into one branch. It carries this PR's change and its test, reworked where needed (see the commit in #39488 that names this PR). Closing this one in its favor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants