Skip to content

bake: report a server module that JSC rejects as a build error instead of aborting the dev server - #46643

Open
robobun wants to merge 8 commits into
mainfrom
robobun/e2939f01/dev-server-chunk-syntax-error
Open

robobun wants to merge 8 commits into
mainfrom
robobun/e2939f01/dev-server-chunk-syntax-error

Conversation

@robobun

@robobun robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #15530

Problem

  • The dev server aborts when JSC rejects a server chunk: panic: Error thrown while evaluating server code. This is always a bug in the bundler. An invalid regular expression (/+/) in a route does it, and so does any other early error that Bun's parser does not check.
  • Cause: finalize_bundle panics on any error from the chunk load (src/runtime/bake/dev_server/finalize.rs:729).

Fix

  • load_server_chunk parses each module of a rejected chunk alone with JSC. A rejected module is a build error at its source position, and the rest load.
  • JSC's chunk-level error is not used: for (a = /+/) => a it names the wrapper.
  • A chunk that JSC parses but does not run (a Proxy in the global prototype chain) is a build error of each of its files.
  • Verified: 19 new tests in test/bake/dev/bundle.test.ts fail on main. test/bake passes. Self-reviewed twice: 19 concerns raised, 18 addressed (Notes).

Background

  • A chunk is one object literal with a function per server module. JSC parses the whole chunk, then runs it.
  • Considered one program per module (the issue's suggestion): every load pays an evaluation per module. An accepted chunk costs the same as on main.

Downsides

Notes

Repro. A framework route (Bun.serve({ app }) or bun bake) that imports a file with /+/.source. On main the first request prints SyntaxError: Invalid regular expression: nothing to repeat and the panic above, exit 134. A save of the same text into a loaded module does the same.

What the error looks like now (terminal output. The overlay shows the same message, position and code line):

1 | export const test = (text: string, pattern = /+/) => pattern.test(text);
                                                 ^
error: Invalid regular expression: nothing to repeat
    at lib.ts:1:46

note: JavaScriptCore rejected the code that Bun generated for this file. If the file is valid, this is a bug in Bun's bundler.

JSC never parses the file. It parses the code that Bun printed for it, so the note says that. For a valid file the note is the signal that main gave with its panic text.

Which inputs. JSC has early errors that Bun's parser does not check. These abort the release build 1.4.3-canary.1 and are build errors here, each with a test: an invalid regular expression, new.target outside a function, using directly in a case clause, two constructors in a class, a parser stack overflow, and one valid file (a binding that Bun names await in a module with top-level await, the renamer gap of #34361). A check of regular expressions in Bun's lexer would cover only the first.

How a module is judged. The chunk ({ <modules> }) is evaluated once, as on main. Only if that throws, each module is parsed alone as 0,{ <module> } with JSC::checkSyntax (no leading (, nothing is run, nothing is registered). JSC reports the token where it stopped. The position is the last source mapping at or before that token. JSC reads the token after a regular expression before it rejects the expression, so for that message the position is the last mapping before the token. That is the expression, also when the next token is on another line.

The source of a module. Each part of the chunk now carries the bundler's index of its source. A plugin can give the client and the server different text for one file (bun-plugin-svelte does). The file then has more than one source, and a search by file gave the wrong one: the error had no position.

A chunk error that no module explains. JSC does not run a program while a Proxy is in the prototype chain of the global object. User code can set one. Each file of that chunk now fails with The server code of this bundle did not load: Proxy is not allowed in the global prototype chain., and the files load after the prototype is restored. The abort stays for one case: JSC's parser accepts each module alone and rejects the chunk. Then the text that Bun puts around the modules is wrong, which no file can take.

Client component lists. receive_chunk lists a "use client" file for the runtime before JSC sees its code. A rejected "use client" file now leaves client_components_added, so the runtime does not try to load it. If no module is left and a file stopped being a client component in that save, registerUpdate still runs with an empty object, so the manifests lose the entry.

index_failures moved below the chunk load, so a failure recorded there reaches the sockets and the waiting requests in the same round. Nothing between the two places reads what it writes.

Deleted. BakeLoadServerHmrPatch and its Rust wrapper had one caller: the arm for a chunk with code and no source map entry. Every part of a chunk has an entry, so the arm was unreachable.

Source::line_col_to_byte_offset returned None for the start position. It now returns 0. The other caller (snapshot.rs) handles the equal case before the call.

Tests. Three of the new tests were checked against a build with one change undone each. With the lookup for regular expressions undone, the expression at the end of a line is reported at lib.ts:5:1 and not lib.ts:4:3. With the search by file back, the plugin case prints Comp.ts: error: with no position. With the abort back, the Proxy case panics.

Measurements (main 5c6d4e2 and this branch at 557ebb1, linux-x64. The later commits add about 90 lines of Rust and were not measured again):

  • Evaluations, counted with a scratch counter over bundle.test.ts (64 tests): every accepted chunk is evaluated once and no module is parsed alone (69 chunks). A rejected chunk of n modules: 2 evaluations and n module parses, for any number of rejected modules. 1 evaluation if no module is left.
  • 44 inputs that main's dev server accepts and strict code would reject (with, delete name, var arguments, and so on): 44 answer 200 on main and 44 answer 200 here, with the same body. This change does not touch code that works.
  • Release build: .text 82,231,705 to 82,234,777 (+3,072), .rodata +0, .data +0.
  • Syscalls and instruction counts are not measured. strace, perf and valgrind are not installed in the build container. The accepted path adds no call.
  • test/bake, debug build, with --timeout 120000 (a React production build takes 5 to 11 s under ASAN on this machine, so production.test.ts and two react-spa.test.ts tests time out at the default 5 s with and without the change): 289 tests in 26 files pass at 878019b. Release build at 557ebb1: 288 pass, 0 fail. incremental-graph-edge-deletion.test.ts failed once in four runs on this branch and once in two on main.
  • The tests run the base case and the multi-file case with BUN_ASSUME_PERFECT_INCREMENTAL 0 and 1. With 0 (the release default) each request for a failed route bundles the route again, so the error prints again.

Not in this change.

  • Client chunks. The browser parses them, and the dev server does not. On a first load with /+/ in client code the page is the static HTML, with no overlay and no error on the terminal. On a hot update the overlay and the terminal show the browser's SyntaxError with no file or line.
  • A module that has its own parentheses around an arrow function with the rejected code, such as an arrow function that is called at once. JSC reads the parentheses again as arrow parameters and reports that error, at a position in valid code. The build error names the right file with that message.
  • Source maps of server chunks. Every server chunk has the URL bake://server.patch.js, so a chunk that registers replaces the source map of the chunks before it, also a rejected one. After a second chunk loads, the frames of a module from the first chunk print as bake://server.patch.js:45:22 (release build 1.4.3-canary.1 and this branch). bake: give every server HMR patch its own source URL so older patches keep remapping #37440 gives each chunk its own URL.
  • Build errors in a file that a route reaches only through the server entry point: dev server: look through the server entry point for build errors and client components #42922.
  • A "use client" file that re-exports a rejected file with export *. After the fix of the rejected file the runtime still prints Failed to load bundled module until the "use client" file is saved. A parse error of Bun in the same place does the same on main.
  • BUN_ASSUME_PERFECT_INCREMENTAL=1, the default of debug builds, with a chunk that JSC did not run. In that mode a request does not build a failed route again (ensure_route.rs:431), so the files of that chunk stay failed until each is saved. With 0, the release default, the next request builds the route again. The failure does not come from the text of a file, and the dev server has no failure that belongs to a route or a bundle.
  • A file nested within a few levels of the depth that JSC's parser takes. The chunk adds parser frames to each module, so JSC can reject the chunk and accept each module alone. That case still aborts, as every depth past the limit does on main. The test for the parser's stack uses twice the limit.
  • Bytecode generation drops the same ParserError (ZigSourceProvider.cpp:250).
  • fix(devserver): replace panic with graceful error handling for server code evaluation errors #28226 was an earlier attempt (log the error and keep the old code). It had no maintainer review.

Self-review. The first review raised 10 concerns. Addressed: blame by the position of the chunk-level error (each module is now parsed alone), line counting that differed from JSC's (no line lookup is left), the client component lists, a parser stack overflow in a module, tests for both incremental modes, a note on the failure, a test with a connected client, a test for a later chunk with accepted and rejected modules, and packaging (strict mode for server modules and a renamer change are not part of this PR). Not taken then: the abort for a chunk error that no module explains.

The second review, of the PR as it stood at 06c4f4f, raised 9. Addressed: a commit that moved the registration of a chunk's source map is reverted (#37440 removes the cause), the source of a part is carried and not searched, the note no longer says that JSC rejected the file, rows without a regular expression and the valid file are tested, the parser stack overflow is tested, an unknown parser error type is a rejection, and the body names the inputs, the late position and the client behaviour as measured. The abort that the first review left is now a build error when the error is not from the parser. Not done: no issue is filed for client chunks. They are described under "Not in this change".

@robobun

robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status. How I reproduced it:

  1. A framework route that imports a file with an invalid regular expression:

    // lib.ts
    export {};
    globalThis.value = /+/.source;
    // routes/index.ts
    import "../lib";
    export default () => new Response("value: " + globalThis.value);
  2. On main (release build of 5c6d4e2 and on 1.4.3-canary.1), the first request prints SyntaxError: Invalid regular expression: nothing to repeat and panic: Error thrown while evaluating server code. This is always a bug in the bundler. The process exits with code 134. A save of the same text into a loaded module does the same.

  3. With this branch the request answers 500 with the build error at lib.ts:2:20, and the route answers 200 after the file is fixed.

The 19 new tests in test/bake/dev/bundle.test.ts fail on the release build 1.4.3-canary.1 (367d939), which does not have the change, and pass with this branch.

@robobun

robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:23 PM PT - Oct 24th, 2026

@robobun, your commit fde964f is building: #132208

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 557ebb1 and fde964f.

Files selected for processing: 4
  • src/runtime/bake/BakeSourceProvider.cpp
  • src/runtime/bake/dev_server/finalize.rs
  • src/runtime/bake/dev_server/incremental_graph.rs
  • test/bake/dev/bundle.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

Server HMR now detects JavaScriptCore syntax errors without executing modules. It removes rejected modules from server chunks, maps available errors to source locations, and indexes failures after chunk loading. Regression tests cover error reporting, successful updates, recovery, and client-component manifests.

Changes

Server HMR recovery

Layer / File(s) Summary
Syntax error detection
src/ast/lib.rs, src/runtime/bake/BakeSourceProvider.cpp, src/runtime/bake/dev_server/server_hmr_ffi.rs
The FFI now parses server source without executing it and returns syntax-error details. The source-position conversion returns byte offset zero when the requested position matches the starting position.
Chunk isolation and recovery
src/runtime/bake/dev_server/incremental_graph.rs, src/runtime/bake/dev_server/finalize.rs
The loader evaluates the combined chunk, identifies rejected modules, removes their code and source-map entries, and reevaluates remaining modules. It maps available syntax-error locations to original sources.
Failure indexing and regression coverage
src/runtime/bake/dev_server/finalize.rs, test/bake/dev/bundle.test.ts
The server update callback runs only when chunk loading returns modules. Failure changes are indexed after loading. Tests cover error reporting, updates to other routes, recovery, and client-component manifest changes.

Suggested reviewers: alii, dylan-conway

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Low · up to fde96

The server can recover from a rejected chunk, but subsequent errors in previously loaded code may show generated locations. This is a bounded diagnostics issue rather than a blocker to recovery.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise, specific, and accurately describes the main change: reporting JSC-rejected server modules as build errors instead of aborting the dev server.
Linked Issues check Passed Issue #15530 requires the dev server to stop aborting when JSC rejects generated server code, such as an invalid regular expression, and to identify the source module. load_server_chunk evaluates th…
Out of Scope Changes check Passed The changed code supports issue #15530. Source-index tracking and source-map lookup support file and position attribution. JSC syntax-error FFI supports non-executing module checks. Incremental-graph …
Description check Passed The description is complete and relevant. It explains the problem, cause, fix, design decisions, limitations, test coverage, verification results, and performance measurements. It does not use the tem…
Full details: Linked Issues check

Explanation

Issue #15530 requires the dev server to stop aborting when JSC rejects generated server code, such as an invalid regular expression, and to identify the source module. load_server_chunk evaluates the combined chunk, checks each module with bake_find_server_code_syntax_error after rejection, records a file-specific build error, removes the rejected module, and reloads the remaining modules. The tests cover JSC parser mismatches, source positions, recovery, multiple modules, and hot updates.

Full details: Out of Scope Changes check

Explanation

The changed code supports issue #15530. Source-index tracking and source-map lookup support file and position attribution. JSC syntax-error FFI supports non-executing module checks. Incremental-graph updates, failure indexing, and client-component handling support recovery and runtime consistency. Tests cover the added behavior. No unrelated change is demonstrated.

Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, cause, fix, design decisions, limitations, test coverage, verification results, and performance measurements. It does not use the template headings exactly, but it provides the required information in equivalent sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

A second branch for this issue exists: robobun/1f253297/dev-server-chunk-eval-failure (d934d98). No PR is open for it, because this PR covers #15530.

The branch has the same per-module JSC::checkSyntax check and the same per-file build error. The differences:

  • It loads the server chunk before finalize_bundle gives the files to the incremental graph. A rejected file then takes the print-failure arm of Pass 1, and nothing is removed from the graph afterwards.
  • A chunk that throws for another reason (a Proxy in the prototype chain of the global object, a terminated worker) becomes a failure of each of its files. The same error from the load of the server runtime goes to the caller of Bun.serve.
  • It registers the source map of a chunk only after the chunk loaded.

Tests: 8 new tests in test/bake/dev/bundle.test.ts and 1 in test/bake/app-options.test.ts. They pass on a debug build of the branch. They fail on the release build 1.4.3-canary.1 (367d939), which does not have the change.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline note, I also checked: the source-map JSON handed to BakeLoadServerHmrPatchWithSourceMap is owned by the DevServerSourceProvider whether or not executeProgram throws, so the first failed evaluation does not leak it; the +1 ref message out-param from BakeFindServerCodeSyntaxError is released by bun_core::String's Drop; take_js_bundle_server/take_source_map do not consume current_chunk_code/current_chunk_source_maps, so the re-evaluation after remove_current_chunk_part sees exactly the remaining parts, and current_chunk_len is decremented by the same per-part length receive_chunk added; JSC::checkSyntax returns through ParserError without touching the VM exception slot.

Extended reasoning...

The change replaces the dev server's abort-on-JSC-rejection with per-module checkSyntax triage in src/runtime/bake/dev_server/finalize.rs, adds a new C++ FFI entry in src/runtime/bake/BakeSourceProvider.cpp, deletes the dead BakeLoadServerHmrPatch, and adds ten devTest cases; it touches no auth, crypto, or injection surface. A human look is still worthwhile because it is a ~500-line native change across the Rust/C++ boundary with ownership transfers, an index-failures reordering, and process-abort paths, and because the one inline finding concerns untested separateSSRGraph coverage of the new remove_current_chunk_part guard.

Comment thread src/runtime/bake/dev_server/incremental_graph.rs
Comment thread src/runtime/bake/BakeSourceProvider.cpp Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/incremental_graph.rs Outdated
Comment thread src/runtime/bake/dev_server/incremental_graph.rs Outdated

@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.

I reviewed the updated PR and didn't find any bugs; the separateSSRGraph test added in af1d25e covers the two-parts-per-file case from my earlier inline comment. Because this rewires the server-chunk load path (a new JSC FFI entry, abort paths, source-map position mapping) and changes Source::line_col_to_byte_offset for every caller, a human look would still be worthwhile.

What was reviewed:

  • Ownership across the new FFI: the ManuallyDrop source-map JSON in evaluate_server_chunk reaches C++ on every call including the retry, and the +1 message ref from BakeFindServerCodeSyntaxError is released by bun_core::String's Drop when ServerCodeSyntaxError goes out of scope.
  • BakeLoadServerHmrPatch deletion: no declaration or caller remains under src/; the clone_latin1 chunk path it served is gone with it.
  • The index_failures() move and the client_components_added retraction in remove_current_chunk_part: traced against the empty-chunk and SSR-graph tests, which exercise both the removed-component and proxy-kept outcomes.
Extended reasoning...

The change touches the Bake dev server's server-chunk load (src/runtime/bake/dev_server/finalize.rs, incremental_graph.rs, server_hmr_ffi.rs), replaces a C++ executor with a JSC checkSyntax entry in BakeSourceProvider.cpp, adds an early return to Source::line_col_to_byte_offset in src/ast/lib.rs, and adds ten dev-server tests. It touches no auth, crypto, or injection surface; the untrusted input is the user's own source text fed to JSC's parser. Since the prior review the only change is the test commit addressing my inline nit, and the bug hunt ran dry with no findings. Approval is withheld because the diff is about 530 lines of non-mechanical FFI, abort and source-map logic plus a shared-helper semantics change, which is beyond what an automated review should sign off alone; CODEOWNERS covers only *.d.ts, so no owner-gated path is involved.

…loads

A chunk that JSC rejects no longer takes the place of the source map of
the last chunk that loaded, so the stack traces of the code that runs
stay mapped.
@robobun

robobun commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Changes since the last review (557ebb1 to 06c4f4f):

  • af1d25e adds a test with a separate SSR graph. It covers the case where one file has two parts in the chunk, which the guard in remove_current_chunk_part exists for.
  • b32b61b makes each comment that the comment check flagged one line.
  • fdde0bb registers the source map of a server chunk only after the chunk loads. Before, a chunk that JSC rejected replaced the source map of the last chunk that loaded, and the frames of the code that runs printed as bake://server.patch.js:22:22. The new test "a hot update that JSC rejects keeps the source map of the code that runs" fails when the registration moves back before the load.
  • 06c4f4f merges main (811afe7, WebKit 680ef47cec2c).

On a debug build of 06c4f4f, test/bake passes with --timeout 120000: 282 tests in 26 files.

@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.

Code review found no issues

No high-confidence issues detected in this change.

…e chunk loads"

This reverts commit fdde0bb. Every server chunk has the same source
URL, so any chunk that registers replaces the source map of the chunks
before it. #37440 gives each chunk its own URL, which removes the cause.
… the source of rejected code from its chunk part

- A server chunk that throws while JSC accepts its syntax fails each of
  its files. The abort stays for a chunk that JSC parses module by module
  and not as a whole.
- Each part of a server chunk carries the bundler's index of its source.
  A search by file gave the wrong source when a plugin gives the client
  and the server different text.
- The position of an invalid regular expression is the last mapping
  before the token where JSC stopped, which JSC reads before it rejects
  the expression.
- The note says that JSC rejected the code that Bun generated.
- Every parser error type is a rejection.
- Tests: inputs with no regular expression, a valid file, a parser stack
  overflow, and a plugin that gives each side its own text.
@robobun

robobun commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Changes after a second self-review (06c4f4f to fde964f):

  • 6d7a74d reverts the commit that moved the registration of a chunk's source map. Every server chunk has the same source URL, so that commit covered one of four ways to lose the mapping. bake: give every server HMR patch its own source URL so older patches keep remapping #37440 gives each chunk its own URL.
  • 878019b:
    • A chunk that JSC parses but does not run is a build error of each of its files. A Proxy in the prototype chain of the global object does that. The abort stays only when JSC accepts each module alone and rejects the chunk.
    • Each chunk part carries the bundler's index of its source. With a plugin that gives the client and the server different text for one file, the error had no position.
    • An invalid regular expression is reported at the expression, also when the next token is on another line.
    • The note now says: JavaScriptCore rejected the code that Bun generated for this file. If the file is valid, this is a bug in Bun's bundler.
    • New test rows have no regular expression: new.target outside a function, using directly in a case clause, two constructors in a class, and one valid file (until bundler: reserve await, eval and arguments in esm output and for bundler-made bindings #34361). New tests cover the Proxy case, a parser stack overflow and the plugin case.
  • fde964f merges main (179664e).

The 19 new tests fail on the release build 1.4.3-canary.1 and pass on a debug build of the branch. test/bake passes on a debug build at 878019b (289 tests, --timeout 120000).

Build 131860 had two failed jobs, and neither ran code of this change. test/js/first_party/ws/ws-proxy.test.ts on Windows 2019 x64 also fails on main builds 131841 and 131489. The darwin aarch64 job stopped at buildkite-agent artifact download timed out after 120s before it ran a test.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines +297 to +309
while !dev.server_graph.current_chunk_code.is_empty() {
let module = dev.server_graph.remove_current_chunk_part(0);
let mut log = Log::init();
log.add_error_fmt(
Some(&sources[module.source_index.get() as usize]),
bun_ast::Loc::EMPTY,
format_args!(
"The server code of this bundle did not load: {}",
bstr::BStr::new(&message)
),
);
fail_server_file(dev, module.file_index, &log)?;
}

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.

🟡 (optional) Under BUN_ASSUME_PERFECT_INCREMENTAL=1 (the debug-build default), a server chunk that throws at load leaves every one of its files failed until each is re-saved, node_modules included. fail_server_chunk at finalize.rs:297-309 records a file-level failure for every part when the chunk throws for a reason no file explains. With perfect-incremental on, a request to a failed route stops at ensure_route.rs:246-251 instead of rebuilding, and only a watcher edit of that exact file clears its failure. Fix: on a chunk-level runtime failure, mark the chunk's files stale so the next request rebuilds and re-evaluates, rather than inserting per-file failures that only an edit of each file removes.

Why this was flagged

A dev server runs with BUN_ASSUME_PERFECT_INCREMENTAL=1, which lifecycle.rs:104-108 makes the default when IS_DEBUG. A bundle's server chunk parses but executeProgram throws (the PR's own test trigger is a Proxy in the global prototype chain). load_server_chunk finds no part that fails alone, so fail_server_chunk (finalize.rs:297-309) calls fail_server_file for every part, inserting a failure keyed by that file into bundling_failures and setting its stale bit. On the next request, ensure_route.rs:246-251 calls check_route_failures; with assume_perfect_incremental_bundling true it returns Stop (ensure_route.rs:448-449), so the chunk is never re-evaluated even after the condition that threw is gone. The only thing that clears a file's failure is receive_chunk of that file (incremental_graph.rs:765-776). On the base branch the process aborted; with =0 (release default) each request rebuilds and recovers, so the trap exists only in the =1 state, which the new test for this path (bundle.test.ts:2148) does not exercise.

Verification: fail_server_chunk (src/runtime/bake/dev_server/finalize.rs:297-309) calls fail_server_file for every part, and insert_failure sets stale_files, failed = true and records a per-file entry in bundling_failures. On the next request, ensure_route.rs:246-251 calls check_route_failures, which under assume_perfect_incremental_bundling returns Stop (ensure_route.rs:448-449).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is right, and this push does not change it.

With BUN_ASSUME_PERFECT_INCREMENTAL=1, check_route_failures answers a route that has a failed file with the failures and returns Stop (ensure_route.rs:431-449). The files of such a chunk then stay failed until each one is saved. With 0, the release default, the next request builds the route again, and the test for this path runs in that mode. Before 878019b this input aborted the process in both modes.

The failure does not come from the text of a file, so it does not fit a failure that belongs to a file. The fix needs a failure that a request can clear, one that belongs to a route or to a bundle. serialized_failure::Owner::Route has no producer, and index_failures treats it as unreachable (failures.rs:237). I did not add that state to this PR. The description now lists the limit under "Not in this change".

Comment on lines +283 to +287
if server_hmr_ffi::bake_find_server_code_syntax_error(global, BunString::clone_utf8(&chunk))
.is_some()
{
// JSC parses each module alone, so the text that Bun puts around the modules is wrong.
abort_on_server_chunk_error(dev, exception);

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.

🟣 pre-existing, not blocking: pre-existing: a server file nested a few levels short of JSC's parser stack limit still aborts the dev server, while one level deeper gets a clean build error. Each part is checked as 0,{…} (finalize.rs:218) but the chunk runs as ({…}), about 7 more parser frames, and executeProgram parses deeper still. In that band no part is rejected, so the whole-chunk check at finalize.rs:283-287 sees a StackOverflow and calls abort_on_server_chunk_error. Fix: make the per-part check use at least as much stack as the chunk evaluation (extra wrapper nesting, e.g. 0,[[[{…}]]]) and never abort on a StackOverflow-typed chunk rejection, which also stops the shallower band from failing every file with "The server code of this bundle did not load".
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

A developer saves a server file whose block or expression nesting is just below the depth at which JSC's parser overflows with the dev server's BUN_JSC_maxPerThreadStackUsage. JSC's parser checks the absolute stack pointer at every recursive parse call (VM::isSafeToRecurse), so the same text overflows at one call depth and parses at a shallower one. The chunk is evaluated by executeProgram, then parsed as ({ ... }); each part alone is parsed by checkSyntax as 0,{ ... } (incremental_graph.rs:1799-1810), which reaches the module body with fewer frames. For depths where the chunk overflows and the parts do not, the loop at finalize.rs:217-254 rejects nothing, rejected_any stays false, and fail_server_chunk at finalize.rs:283 re-parses the whole chunk, gets a StackOverflow error from BakeFindServerCodeSyntaxError (BakeSourceProvider.cpp:91-99), and aborts the process at finalize.rs:287, as the base branch does for every depth past the limit.

Verification: pre-existing. Trigger: a server file whose nesting depth falls in the band where JSC's parser overflows when the whole chunk is parsed as ({…}) but not when the single part is parsed alone as 0,{…}. The per-part loop at finalize.rs:217-225 finds no part rejected, so fail_server_chunk at :278-287 re-parses the whole chunk, overflows again, and abort_on_server_chunk_error panics. The base branch already aborts here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct. The chunk adds parser frames to each module (({ against 0,{, and the frames of executeProgram). In a band of a few nesting levels JSC rejects the chunk and accepts each module alone, and fail_server_chunk then takes the abort. Main aborts there too, as it does for every depth past the limit. The test for the parser's stack uses twice the limit, so it does not reach the band.

A wrapper for the per-module parse that nests deeper than the chunk (0,[[[[{) removes the abort in that band. It can then reject a module that the chunk loads, but only when the chunk already failed for another reason. I have no deterministic test for a depth inside the band, and CI runs on the current head, so I did not push that change alone. The description lists the band under "Not in this change". The change goes in with the next push to this branch.

This branch has not been deployed

No deployments
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.

devserver: regular expression syntax errors are not caught by bundler

1 participant