Skip to content

Bun.serve: validate per-method route values like top-level route values - #36311

Open
robobun wants to merge 6 commits into
mainfrom
farm/e50a5517/serve-method-route-validation
Open

robobun wants to merge 6 commits into
mainfrom
farm/e50a5517/serve-method-route-validation

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

What

Bun.serve({ routes }) validates top-level route values: { "/x": 42 } throws a TypeError at registration. The same invalid value nested inside a method object was accepted silently, leaving that method dead (falls through to fetch or 404) with no error at registration or request time.

Bun.serve({ routes: { "/x": 42 } });                          // throws
Bun.serve({ routes: { "/x": { GET: 42, POST: handler } } });  // accepted, GET is dead

The realistic face of this is a typo'd handler import, e.g. { GET: handlers.getUsr }, producing a route that silently never runs.

Why

The per-method loop in ServerConfig::from_js set found = true for any own method key before checking the value type. A value that was neither callable nor a valid AnyRoute fell out of the if/else if without an else, and found = true then suppressed the top-level validator. Panic/top-frame signature: Invalid value for route "/x" method GET.

Fix

Per-method values now match top-level semantics:

  • undefined skips the key (same as top-level "/x": undefined)
  • false marks the object as a method map without registering a handler (documented disable)
  • anything else that is not a callable or a valid static route throws Invalid value for route "<path>" method <METHOD>. Expected a function, Response, HTMLBundle, BunFile, or false.

Verification

USE_SYSTEM_BUN=1 bun test test/js/bun/http/bun-serve-routes.test.ts -t "per-method route value validation"
  6 fail (null/number/string/object/array/true accepted silently)

bun bd test test/js/bun/http/bun-serve-routes.test.ts
  64 pass, 0 fail

[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-routes.test.ts
bun test v1.4.0 (2c84eecb7)

test/js/bun/http/bun-serve-routes.test.ts:
(pass) path parameters > handles single parameter [25.19ms]
RequestParams {
  postId: "456",
  commentId: "789",
}
(pass) path parameters > handles multiple parameters [17.55ms]
(pass) path parameters > handles encoded parameters [8.57ms]
(pass) path parameters > handles unicode parameters [13.46ms]
(pass) path parameters > decodes raw valid UTF-8 bytes in a parameter segment [376.50ms]
(pass) path parameters > decodes raw an invalid UTF-8 byte in a parameter segment [53.18ms]
(pass) HTTP methods > GET request [24.82ms]
(pass) HTTP methods > POST request [5.46ms]
(pass) HTTP methods > PUT request [10.76ms]
(pass) HTTP methods > DELETE request [4.73ms]
(pass) HTTP methods > PATCH request [3.71ms]
(pass) HTTP methods > OPTIONS request [9.83ms]
(pass) HTTP methods > HEAD request [5.64ms]
(pass) implicit HEAD for per-method route objects > HEAD is served by the GET handler when no HEAD handler is declared [59.42ms]
(pass) impli
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9356b6a24)

test/js/bun/http/bun-serve-routes.test.ts:
(pass) path parameters > handles single parameter [1.37ms]
RequestParams {
  postId: "456",
  commentId: "789",
}
(pass) path parameters > handles multiple parameters [0.31ms]
(pass) path parameters > handles encoded parameters [0.14ms]
(pass) path parameters > handles unicode parameters [0.15ms]
(pass) path parameters > decodes raw valid UTF-8 bytes in a parameter segment [5.55ms]
(pass) path parameters > decodes raw an invalid UTF-8 byte in a parameter segment [0.73ms]
(pass) HTTP methods > GET request [1.06ms]
(pass) HTTP methods > POST request [0.14ms]
(pass) HTTP methods > PUT request [0.08ms]
(pass) HTTP methods > DELETE request [0.07ms]
(pass) HTTP methods > PATCH request [0.07ms]
(pass) HTTP methods > OPTIONS request [0.07ms]
(pass) HTTP methods > HEAD request [0.10ms]
(pass) implicit HEAD for per-method route objects > HEAD is served by the GET handler when no HEAD handler is declared [1.20ms]
(pass) implicit HEAD for per-method route objects > HEAD does not 404 when there is no later route to fall through to [0.79ms]
(pass) implicit HEAD for per-method route objects > the GET 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-routes.test.ts
bun test v1.4.0 (2c84eecb7)

test/js/bun/http/bun-serve-routes.test.ts:
(pass) path parameters > handles single parameter [22.90ms]
RequestParams {
  postId: "456",
  commentId: "789",
}
(pass) path parameters > handles multiple parameters [10.06ms]
(pass) path parameters > handles encoded parameters [13.51ms]
(pass) path parameters > handles unicode parameters [7.35ms]
(pass) path parameters > decodes raw valid UTF-8 bytes in a parameter segment [365.80ms]
(pass) path parameters > decodes raw an invalid UTF-8 byte in a parameter segment [51.35ms]
(pass) HTTP methods > GET request [23.04ms]
(pass) HTTP methods > POST request [4.84ms]
(pass) HTTP methods > PUT request [3.86ms]
(pass) HTTP methods > DELETE request [12.60ms]
(pass) HTTP methods > PATCH request [4.19ms]
(pass) HTTP methods > OPTIONS request [10.30ms]
(pass) HTTP methods > HEAD request [5.05ms]
(pass) implicit HEAD for per-method route objects > HEAD is served by the GET handler when no HEAD handler is declared [50.34ms]
(pass) impl
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 698ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[1/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_runtime v0.0.0 (/workspace/bun/src/runtime)
�[1m�[92m   Compiling�[0m bun_bin v0.0.0 (/workspace/bun/src/bun_bin)
�[1m�[92m    Finished�[0m `release` profile [optimized + debuginfo] target(s) in 4m 20s
[2/5] link bun-profile
[4/5] strip bun
[4/5] bun-profile --revision
1.4.0-canary.1+2c84eecb7
[build] done
bun test v1.4.0-canary.1 (2c84eecb7)

test/js/bun/http/bun-serve-routes.test.ts:
(pass) path parameters > handles single parameter [1.50ms]
RequestParams {
  postId: "456",
  commentId: "789",
}
(pass) path parameters > handles multiple parameters [0.28ms]
(pass) path parameters > handles encoded parameters [0.14ms]
(pass) path parameters > handles unicode param
... (truncated)
diff hotspot
src/runtime/server/ServerConfig.rs        | 29 +++++++++-
 test/js/bun/http/bun-serve-routes.test.ts | 95 +++++++++++++++++++++++++++++++
 2 files changed, 122 insertions(+), 2 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                       reads  edits  tests
src/runtime/server/ServerConfig.rs             5      6      0
test/js/bun/http/bun-serve-routes.test.ts      4      4      0

A route value that is not a function/Response/HTMLBundle/BunFile/false
throws a TypeError at registration time. The same invalid value nested
inside a method object ({GET: 42}, {GET: null}, {GET: "str"}, etc.) was
accepted silently and the method became dead: requests fell through to the
fetch fallback with no error at registration or request time.

The method loop set found=true for any own method key before checking the
value type, so an unrecognized value skipped both the callback and static
route branches and fell out of the if/else without error, then found=true
suppressed the top-level validator.

Now per-method values match top-level semantics: undefined skips the key,
false marks the object as a method map without registering a handler, any
other value that is not a callable or a valid static route throws with the
path and method name.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Bun.serve() now skips undefined and false per-method route values, rejects unsupported values with invalid_arguments, preserves HEAD: false behavior, and adds coverage for validation, fallback handling, and direct Response values.

Changes

Per-method route validation

Layer / File(s) Summary
Parse and validate per-method values
src/runtime/server/ServerConfig.rs, test/js/bun/http/bun-serve-routes.test.ts
Route parsing skips undefined and false, reports unsupported values with their received type and expected types, suppresses implicit HEAD for HEAD: false, and tests accepted and rejected per-method values.

Possibly related PRs

  • oven-sh/bun#36319: Updates related ServerConfig.rs route parsing for per-method HEAD: false semantics and validation.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly describes the main change: validating per-method route values in Bun.serve.
Description check ✅ Passed The description covers the behavior change, rationale, and verification, though it uses custom headings instead of the template's exact sections.

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

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on stock 1.4.0-canary: { "/x": { GET: 42, POST: h } } is accepted and GET falls through to fetch. With this change it throws TypeError: Invalid value for route "/x" method GET: received number. Expected a function, Response, HTMLBundle, BunFile, or false. at registration.

Follow-ups from review, all addressed:

  • 52b3a5c: a lone { GET: undefined } / { GET: false } is still accepted (recognized as a method map, nothing registered); route name is validated only on the first real handler; the error names the received typeof.
  • 9356b6a: { GET: h, HEAD: false } now sends HEAD to fetch instead of the GET-derived route. The static-GET sibling ({ GET: new Response(...), HEAD: false }) needs a per-path signal threaded into apply_static_route and is tracked separately as Bun.serve: honour per-method false as a negative route #36319.

bun-serve-routes.test.ts (67), bun-serve-args.test.ts (59), and bun-serve-static.test.ts (46) pass locally and in CI on every lane.

CI status: the diff is green. Build 84881 failures are unrelated to this change: test/js/bun/http/serve.test.ts segfault at 0xDEADBEEF on darwin aarch64 in the HEAD+ReadableStream response path (config parsing is not on that path; bun-serve-routes.test.ts passed 67/67 on the same lane), and test/cli/install/bun-info.test.ts LSan leak in bun_http::HTTPClient::clone_metadata (HTTP client, bun pm view). Both reported for main-break triage. Remaining failures are batch-only flakes that passed on retry and an Azure agent-creation infra failure. Ready for a maintainer.

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/server/ServerConfig.rs`:
- Around line 972-977: Update the invalid route-handler error construction in
the ServerConfig route validation branch to include a non-coercing description
of the rejected value or its stable JavaScript type, while preserving the route,
method, accepted representations, and remedy guidance in the message. Use the
existing value-formatting or type-description utility if available rather than
coercing the value directly.
- Around line 924-933: Update the method-entry handling around the existing
found flag so undefined and false sentinel values are skipped before route-name
validation, while tracking recognized method entries separately from whether a
route name has been validated. Ensure a lone undefined method does not fall
through to AnyRoute::from_js, false method entries do not validate names, and
the first real handler or static route validates the name; add coverage for lone
undefined and invalid-name-with-false cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1eb75c76-9c41-47cc-9312-67872a2a743a

📥 Commits

Reviewing files that changed from the base of the PR and between 59242d6 and 4908ac0.

📒 Files selected for processing (2)
  • src/runtime/server/ServerConfig.rs
  • test/js/bun/http/bun-serve-routes.test.ts

Comment thread src/runtime/server/ServerConfig.rs
Comment thread src/runtime/server/ServerConfig.rs
…ved type in error

Split found (any method key present) from route_name_validated (checked
once before the first real handler) so a lone {GET: undefined} or
{GET: false} is recognized as a method map and does not fall through to
the top-level validator, preserving the pre-existing behavior for those
sentinels. The error message now names the received typeof (null special
cased).
@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:14 AM PT - Jul 29th, 2026

❌ @robobun, your commit 2c84eec has 3 failures in Build #84881 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36311

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

bun-36311 --bun

Comment thread src/runtime/server/ServerConfig.rs
Comment thread src/runtime/server/ServerConfig.rs
Per-method false means the method is explicitly unhandled and falls through
to fetch. The false arm now sets has_head_route for Method::HEAD so the
derived HEAD route is not pushed after the loop, matching the callable and
static-route arms.
Comment thread src/runtime/server/ServerConfig.rs

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/server/ServerConfig.rs`:
- Around line 936-939: Update the route-processing flow around
validate_route_name so the method value is first confirmed as callable or an
allowed static route before validating route parameter names. Ensure invalid
method values produce the method-specific TypeError before any route-name error,
and add coverage for a route such as "/:1" with GET set to a number.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 006d9e18-ebcf-4321-93b7-007f0b81c5fb

📥 Commits

Reviewing files that changed from the base of the PR and between 4908ac0 and 2c84eec.

📒 Files selected for processing (2)
  • src/runtime/server/ServerConfig.rs
  • test/js/bun/http/bun-serve-routes.test.ts

Comment thread src/runtime/server/ServerConfig.rs

@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. All earlier review threads are addressed (52b3a5c, 9356b6a); the remaining static-GET HEAD: false nit is not a regression and is reasonably deferred to a follow-up.

What was reviewed:

  • found = true now precedes the undefined/false skips, so a lone { GET: undefined|false } is recognized as a method map and does not fall through to the top-level validator — covered by the new lone-sentinel tests.
  • The false arm sets has_head_route for Method::HEAD, matching the callable and static sibling arms; covered by the new implicit-HEAD test.
  • The new error path uses js_type_string (non-throwing, SmallStrings-backed) with a null special-case, and throw_invalid_arguments returns Err so nothing allocated above leaks.
  • route_name_validated is separate from found, so sentinel-only method maps never trigger validate_route_name on paths like /:1.
Extended reasoning...

Overview

This PR closes a validation gap in ServerConfig::from_js: per-method route values ({ "/x": { GET: <value> } }) are now validated with the same semantics as top-level route values. The Rust change is ~25 lines confined to the METHODS loop in the routes-object parse path — a new route_name_validated flag, undefined/false sentinel handling placed after found = true, a has_head_route = true write in the false arm for Method::HEAD, and an else branch that throws a descriptive invalid_arguments error naming the route, method, received typeof, and accepted types. The test change adds a describe block covering rejection of null/number/string/object/array/true, acceptance of undefined/false (both mixed and lone), a HEAD: false suppression test, and a positive control that a per-method Response is still accepted.

Security risks

None. This is registration-time input validation on a user-supplied config object; no untrusted network data, no auth/crypto/permissions. js_type_string is documented as non-throwing (SmallStrings-backed) so the error path cannot enter user JS. The change tightens validation rather than loosening it.

Level of scrutiny

Medium. Bun.serve is a core API, but the change is on the cold config-parse path, not the request-handling hot path, and it only adds an else to an existing if/else if chain plus reorders two flag writes. The behavior change is that previously-silently-dead per-method values now throw at registration — a strict improvement for the typo'd-import case the PR describes. The two shapes that could plausibly appear in working code (undefined from a conditional, false as a disable) are explicitly preserved and tested, so no realistic working config is broken.

Other factors

Three rounds of review feedback (CodeRabbit ×2, my prior runs ×3) have all been addressed or consciously deferred: the lone-{GET: undefined} regression, the missing received-type in the error, and the callable-GET HEAD: false suppression are fixed with tests; the static-GET HEAD: false sibling is deferred because it requires threading a per-path signal into apply_static_route via StaticRouteEntry/setup_routes, which is out of scope for a validation change and is not a regression (identical observable behavior to pre-PR, RFC-9110-compliant). Test coverage is thorough (it.each matrices for both accept and reject paths, lone-sentinel cases, positive control), and the PR body shows USE_SYSTEM_BUN=1 fails / debug+release pass. No outstanding unresolved threads.

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.

2 participants