Skip to content

Validate app.bundlerOptions values are objects in Bun.serve#30446

Closed
robobun wants to merge 1 commit into
mainfrom
farm/b6a80391/bake-bundler-options-validation
Closed

Validate app.bundlerOptions values are objects in Bun.serve#30446
robobun wants to merge 1 commit into
mainfrom
farm/b6a80391/bake-bundler-options-validation

Conversation

@robobun

@robobun robobun commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Passing a non-object value for app.bundlerOptions, or for its server / client / ssr / minify sub-options, hit a debug assertion in JSValue.get (target.isObject()) instead of throwing a proper error.

Bun.serve({ app: { bundlerOptions: 5 } });
Bun.serve({ app: { bundlerOptions: { server: 5 } } });
Bun.serve({ app: { bundlerOptions: { server: { minify: 5 } } } });

All three now throw a TypeError with a descriptive message rather than panicking.

Also fixes minify: false which previously fell through the boolean check and attempted a property lookup on the boolean value.

Found by Fuzzilli (fingerprint 86855f8bedcf582b).

Passing a non-object value for app.bundlerOptions, or for its server /
client / ssr / minify sub-options, hit a debug assertion in JSValue.get
instead of throwing a proper error.
@robobun

robobun commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:54 PM PT - May 9th, 2026

@robobun, your commit b87f822 has 1 failures in Build #53111 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30446

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

bun-30446 --bun

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96891319-8df6-4d89-8c2d-8fe224055b4a

📥 Commits

Reviewing files that changed from the base of the PR and between fe735f8 and b87f822.

📒 Files selected for processing (2)
  • src/bake/bake.zig
  • test/js/bun/http/bun-serve-args.test.ts

Walkthrough

This PR strengthens bundlerOptions validation by requiring it to be an object and routing its nested subsets (server, client, ssr) through an updated BuildConfigSubset.fromJS function that accepts a compile-time subset name for precise error reporting. Minify parsing is adjusted to explicitly set all minify flags from boolean values, with improved error messages for invalid types.

Changes

Bundler Options Validation

Layer / File(s) Summary
Function Signature Update
src/bake/bake.zig
BuildConfigSubset.fromJS now accepts a compile-time name: []const u8 parameter to enable subset-specific error messages in validation.
Core Validation Logic
src/bake/bake.zig
UserOptions.fromJS validates bundlerOptions is an object and routes each subset (server, client, ssr) to BuildConfigSubset.fromJS with its name. Boolean minify values now directly set all minify flags, and invalid minify types throw errors referencing the subset path.
Test Coverage
test/js/bun/http/bun-serve-args.test.ts
New test suite validates bundlerOptions rejects non-object values, each subset key rejects non-objects, minify throws on invalid types (non-boolean/non-object), and boolean minify does not crash.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description is mostly complete but lacks a 'How did you verify your code works?' section as specified in the template. It provides thorough context about what changed and why, but does not explicitly demonstrate verification steps. Add a 'How did you verify your code works?' section describing test verification, manual testing, or Fuzzilli validation results.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main change: adding validation for app.bundlerOptions values in Bun.serve to ensure they are objects.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Validate app.bundlerOptions types in Bun.serve #30402 - Same bundlerOptions object validation in bake.zig with identical isObject() guards, BuildConfigSubset.fromJS signature change, minify:false fix, and test additions
  2. bake: validate bundlerOptions values are objects before property access #30125 - Same bundlerOptions object validation in bake.zig with equivalent isObject() guards, comptime name parameter, and minify:false fix

🤖 Generated with Claude Code

@robobun

robobun commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30125 and #30402.

@robobun robobun closed this May 10, 2026
@robobun robobun deleted the farm/b6a80391/bake-bundler-options-validation branch May 10, 2026 03:54
Comment on lines +674 to +700
describe("app.bundlerOptions validation", () => {
test.each([5, "hello", true, 1n])("non-object bundlerOptions throws (%p)", value => {
expect(() => {
// @ts-expect-error - Testing runtime validation
serve({ app: { bundlerOptions: value } });
}).toThrow("'app.bundlerOptions' must be an object");
});

test.each(["server", "client", "ssr"])("non-object bundlerOptions.%s throws", key => {
for (const value of [5, "hello", true, 1n]) {
expect(() => {
// @ts-expect-error - Testing runtime validation
serve({ app: { bundlerOptions: { [key]: value } } });
}).toThrow(`'bundlerOptions.${key}' must be an object`);
}
});

test("non-object bundlerOptions.server.minify throws", () => {
for (const value of [5, "hello", 1n]) {
expect(() => {
// @ts-expect-error - Testing runtime validation
serve({ app: { bundlerOptions: { server: { minify: value } } } });
}).toThrow("'bundlerOptions.server.minify' must be a boolean or an object");
}
});

test("boolean bundlerOptions.server.minify does not crash", () => {

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.

🔴 These tests call serve({ app: ... }) in-process, but the app option is gated behind bun.FeatureFlags.bake() (is_canary || isDebug || BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE). The CI runner only sets BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING for the test process — the bake flag is only in harness.ts bunEnv for spawned children — so on a non-canary release build (.buildkite/ci.mjs with canary=0 via [release]) the app block is skipped entirely and serve() throws the generic "Bun.serve() needs either:…" error instead, failing all four new test blocks. Consider setting process.env.BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE = "1" at the top of this describe (the flag is read at runtime), or gating it with describe.skipIf.

Extended reasoning...

What the bug is

The new describe("app.bundlerOptions validation", ...) block in test/js/bun/http/bun-serve-args.test.ts invokes serve({ app: ... }) directly in the test process and asserts on specific error messages such as 'app.bundlerOptions' must be an object and 'app' is missing 'framework'. However, the app option in Bun.serve is feature-flagged: when the bake flag is off, the app key is silently ignored and serve() falls through to the generic missing-handler error. On a non-canary, non-debug release build, none of the asserted messages are ever produced and all four test blocks fail.

Code path

  • src/bun_core/feature_flags.zig:135-138bake() returns env.is_canary || env.isDebug || BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE.get().
  • src/runtime/api/BunObject.zig:1013 passes allow_bake_config = bun.FeatureFlags.bake() into ServerConfig.fromJS.
  • src/runtime/server/ServerConfig.zig:837-841 — when !opts.allow_bake_config / !bun.FeatureFlags.bake(), the entire if (try args.get(global, "app")) |...| block does break :brk, so bake.UserOptions.fromJS is never called and args.bake stays null.
  • Execution then reaches ServerConfig.zig:908-911, which throws Bun.serve() needs either:\n- A routes object\n- A fetch handler... because no fetch/routes/app was registered.

Why existing infra doesn't cover this

The flag is set in test/harness.ts:64 (BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE: "1"), but that object (bunEnv) is only used as the env for spawned child processes. Every existing bake test in test/bake/ spawns a child via the bake harness and inherits bunEnv, so the flag is always present there. These new tests are the first in the repo to call serve({ app: ... }) in-process, so they inherit the test runner's environment instead.

scripts/runner.node.mjs:1182-1202 builds the env for the bun test process by spreading ...process.env and adding BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", BUN_DEBUG_QUIET_LOGS, etc. — but not BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE. These two flags are independent (env_var.zig), so the internal-testing flag does not enable bake.

When it manifests

.buildkite/ci.mjs:1293-1297 sets canary: 0 when the commit message contains [release] or the RELEASE env var is set, and line ~522 passes --canary=off to the build. The resulting binary has env.is_canary = false and env.isDebug = false, so bake() evaluates solely to the env var — which the runner doesn't set. skipTests is an independent option, so the test suite still runs against this binary.

Standard PR CI is unaffected because getCanaryRevision() returns 1 for non-main branches (so is_canary is true and bake() short-circuits to true), which is why this PR's own build will pass. The failure surfaces on the next release-build test job, or for anyone running the suite against a stable release binary locally.

Step-by-step proof

On a release (non-canary, non-debug) binary with the runner's env:

  1. Test calls serve({ app: { bundlerOptions: 5 } }).
  2. BunObject.zig:1013allow_bake_config = bake() = false || false || (env var unset) = false.
  3. ServerConfig.zig:837 reads the "app" property, but ServerConfig.zig:838-840 sees !bun.FeatureFlags.bake() and does break :brkUserOptions.fromJS (and the new isObject() check) never runs.
  4. args.bake is still null, no fetch/routes were provided → is_fetch_required path at ServerConfig.zig:908 throws "Bun.serve() needs either:...".
  5. expect(...).toThrow("'app.bundlerOptions' must be an object") fails because the actual message is "Bun.serve() needs either:...". The same applies to the bundlerOptions.${key}, .minify, and 'app' is missing 'framework' assertions.

Fix

Any of:

  • Add process.env.BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE = "1"; at the top of the new describe (the flag is read via getenv at call time, so setting it from JS before serve() works).
  • Wrap the block with describe.skipIf(...) based on the feature being unavailable.
  • Add BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE: "1" to the env in scripts/runner.node.mjs alongside BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING.

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.

1 participant