Repository navigation
Conversation
Collaborator
Author
|
Updated 10:17 AM PT - Oct 17th, 2026
❌ @robobun, your commit 6005a00 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 45400That installs a local version of the PR into your bun-45400 --bun |
This was referenced Oct 15, 2026
Open
Collaborator
Author
robobun
force-pushed
the
robobun/5627effc/uws-router-method-trees
branch
from
October 15, 2026 20:32
add24f5 to
d0bb702
Compare
robobun
force-pushed
the
robobun/5627effc/serve-method-id-rust
branch
2 times, most recently
from
October 15, 2026 21:57
9e5913c to
ed1a9e9
Compare
robobun
force-pushed
the
robobun/5627effc/uws-router-method-trees
branch
2 times, most recently
from
October 17, 2026 10:06
d5f50df to
0ee2f8e
Compare
robobun
force-pushed
the
robobun/5627effc/serve-method-id-rust
branch
from
October 17, 2026 10:06
ed1a9e9 to
41e84b1
Compare
The parser stores methodIdFromWire() on the request, and HttpRouter keeps one tree per method id plus the tree of the any-method routes. route() indexes the tree of the request's method and then tries the any-method tree. It no longer scans the method names, and it has no positional fallback: a removal cannot make another method's tree take a request. The HTTP/2 and HTTP/3 apps register their routes under the uppercase method names, like HTTP/1.1. Nothing reads the method name of a request any more, so HttpRequest::getMethod() and the lowercase copy in Http3Request go.
robobun
force-pushed
the
robobun/5627effc/uws-router-method-trees
branch
from
October 17, 2026 11:03
0ee2f8e to
6370859
Compare
robobun
force-pushed
the
robobun/5627effc/serve-method-id-rust
branch
from
October 17, 2026 11:03
41e84b1 to
6005a00
Compare
This was referenced Oct 19, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
HttpRouter::route()finds the tree of a method with a scan over the method names. APOSTcosts 291 instructions more than aGET.route()takesroot.children.back()(packages/bun-uws/src/HttpRouter.h:276). That is the any-method node only while no removal erased it. Bun.serve: answer 501 to an unknown method and 404 to a router miss, not a closed socket #45396 and Bun.serve: register the default handler on "/*" for every method #45397 keep requests away from that line.Request::method().Fix
methodIdFromWire()on the request.HttpRouterkeeps one tree per method id, and the any-method tree as a member.route(id, url)indexes.route()refuses an id that is no method, soRequest::method()asserts it in release builds too.static_asserton each.HttpRequest::getMethod()and the lowercase copy inHttp3Requestgo.Behaviour change: none
Tests that cover this code:
test/js/bun/http/bun-serve-routes.test.ts,test/js/bun/http/serve-http2-protocol.test.ts,test/js/bun/http/serve-http3.test.ts,test/js/node/http/node-http.test.ts.Background
route()andcullNode()are uWebSockets code. Bun registers an any-method route under 36 method names, which leaves the any-method node empty, so a removal erases it.Downsides
HttpRoutergrows from 3,360 to 6,528 bytes.Notes
Open question. Open question before this PR leaves draft: the first ask of #6556, a method outside the 36 that reaches
fetch. #44718 (open) lets aRequestcarry any method token. #45399 and this PR remove the method name from the path between uWS and Rust, so a request with no method id has no name to deliver. If maintainers want such a request delivered, the name must stay readable for it. That needs a maintainer decision.Why no answer changes. After #45396 and #45357 a request without a method id does not reach
route(). Without #45357 an unknown:methodover HTTP/3 would get the 404 of a router miss, so this PR is a draft until that PR lands. After #45397 every server has an any-method"/*"registration (the default handler, or a user route), so on HTTP/1.1 each of the 36 methods has a tree, and on HTTP/2 and HTTP/3 the any-method tree is not empty. The fallback line then only ran a second time over any-method handlers that had already yielded. The id-keyed router runs them once.The routers compared. A differential driver ran random registration programs against the router on main and this one: 1.89 million
route()calls in the HTTP/1.1 shape. In every call where the old router stayed in the trees of the request's method, both routers gave the same handlers in the same order. The old router ran a handler that was registered for another method in 61,677 calls (47,709 in the HTTP/2 shape, 87,391 in the HTTP/3 shape). This one: 0.HttpRouter.hin this PR is the file that run measured.#44323 and #35248. #44323 adds a test hook that calls
route(name, url)andadd(names, ...)on this router and pins what the router on main does. One of its tests is the fallback this PR deletes. If #44323 lands first, this PR changes the hook to pass the id, makes it reject a name that is no method beforeadd()can terminate, and updates the expectations that pin the fallback. The expectations for tables that keep an any-method route must not change. #35248 rewrites the storage of the same file and keeps the fallback. It needs a rebase after this PR, or this PR after it.Provenance.
route(),cullNode()and the sort of the method nodes are uWebSockets code. uWebSockets registers an any-method route under"*"and gives an app a default any-method handler, so its any-method node is not empty. Bun's HTTP/1.1 app registers an any-method route under the 36 method names and has no such default, which leaves the node empty.std::terminate()inadd(). A route name that is no method and not"*"terminates at registration. On main such a route was registered under a tree that no request could reach. The names come from string literals inApp.h,Http2App.handHttp3App.h. The HTTP/2 and HTTP/3 literals have astatic_assert. #45397 added a test that registers a route for each of the 9 route-object methods over each protocol.Measurements.
fetch: 2412, 2373. POST tofetch: 2710, 2387. PUT tofetch: 2624, 2379. PROPFIND tofetch: 2644, 2427. UNSUBSCRIBE tofetch: 2709, 2460. GET to a GET route: 2881, 2842. POST to a POST route: 3171, 2848. PUT to a PUT route: 3093, 2848. node:http, GET: 2570, 2513. POST: 2860, 2535. On main (f4d755a) the first two rows are 2435 and 2750. A standalone driver runs the realHttpParser.handHttpRouter.h(clang -O3) underqemu-x86_64 -one-insn-per-tb -d exec,nochain. Repeat runs differ by at most 4 instructions in 2.6 million.perfis not available in the build container (perf_event_paranoidis 4), andvalgrindis not installed.HttpRouter3,360 -> 6,528 bytes.HttpRequest6,472 -> 6,472 (the id goes in padding).Http3Request152 -> 104.operator newcalls) to register 100 any-method routes and the default handler: HTTP/1.1 7,711 -> 7,669. With"/*": { GET: fn }added: 7,713 -> 7,671. HTTP/2 and HTTP/3: 319 -> 319 and 325 -> 323.llvm-size): text 80,693,075 -> 80,695,123 bytes (server: read the method id uWS resolved, not the method name #45399, then this PR, on base f4d755a; measured while the stack had its own HTTP/3 check). data and bss do not change. Against main (80,705,363) the whole stack is 10,240 bytes smaller.Suites (debug build with ASAN):
bun-serve-routes(86 tests),node-http-pipeline-callbacks,node-http,node-http-displaced-response,serve-http2,serve-http2-protocol,serve-http3,request-smuggling,bun-serve-static,bun-serve-file,serve-directory-routes,bun-serve-html, threetest-http-*-client-errorNode tests, andserve.test.ts. Inserve.test.tstwo tests fail in this container on the base too. Innode-http, tests of one group hit their 5 s timeout in the whole-file run. The group has 11 tests that run at once, each with a child process, and they pass when the group runs alone.