Bun.serve: refuse CONNECT before dispatch
Repro
import net from "node:net";
let saw = null;
const srv = Bun.serve({
port: 0, hostname: "127.0.0.1",
fetch(req) { saw = `${req.method} ${req.url}`; return new Response("hi"); },
});
const s = net.connect(srv.port, "127.0.0.1", () =>
s.write("CONNECT h.test:80 HTTP/1.1\r\nHost: h.test:80\r\n\r\n"));
s.on("data", d => console.log(d.toString("latin1").split("\r\n")[0]));
setTimeout(() => { console.log("handler saw:", saw); srv.stop(true); s.destroy(); }, 500);
// before: HTTP/1.1 200 OK
// handler saw: CONNECT h.test:80
// after: handler saw: null (zero response bytes, socket closed)
// node (http.Server, no 'connect' listener): same as after
Cause
HttpParser.h already recognises authority-form CONNECT host:port and threads isConnect through to HttpResponseData::isConnectRequest. The node:http path consumes that flag (routes to the 'connect' event, or closes when there is no listener). Bun.serve never looks at it: the request drops straight into the router, the catch-all /* handler runs with req.url set to the raw authority (new URL("h.test:80") parses as protocol h.test:, pathname "80"), and whatever the handler returns is written to the still-open connection.
RFC 9110 9.3.6: a 2xx to CONNECT means "tunnel established" and MUST NOT carry Content-Length or Transfer-Encoding. A proxy-capable client (or an open-proxy scanner) that reaches a Bun.serve origin gets a success-shaped tunnel response and then ships opaque bytes that Bun parses as the next HTTP request on the kept-alive connection.
Fix
Add a rejectConnect bit to uWS HttpFlags. Inside the request-handler lambda in HttpContext.h, before routing, close the socket (no bytes written) when isConnectRequest && rejectConnect. set_routes() sets the flag whenever the server is not running under onNodeHTTPRequest, so Bun.serve always refuses CONNECT and node:http keeps its existing behaviour. Closing with zero bytes matches Node's http.Server with no 'connect' listener.
The check lives in the shared uWS dispatch path, so it covers every Bun.serve entry point (the fetch handler, routes: {...}, static routes, the no-handler 404) without per-trampoline checks.
Verification
test/js/bun/http/bun-serve-connect.test.ts:
- fetch handler never sees CONNECT; socket closed with no response; a follow-up GET on a fresh connection still works
routes: { "/*": handler }never sees CONNECT- routes-only server (no fetch handler) closes on CONNECT instead of answering 404
- bytes pipelined after CONNECT are discarded, not parsed as HTTP
All four fail on the released bun (the handler receives CONNECT h.test:80 and a 200/404 goes on the wire) and pass with this change. The existing test/js/node/http/node-http-connect.test.ts suite (node:http CONNECT event, tunnel handover, backpressure) still passes.
[review] gate passed · iteration 0 · 7 files touched
fails on main (without fix)
ASAN without fix: 4 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-connect.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (73281ae9d)
test/js/bun/http/bun-serve-connect.test.ts:
40 | const { received, serverClosed } = await sendRaw(
41 | server.port,
42 | "CONNECT h.test:80 HTTP/1.1\r\nHost: h.test:80\r\n\r\n",
43 | );
44 |
45 | expect({ handlerSaw, received, serverClosed }).toEqual({
^
error: expect(received).toEqual(expected)
{
- "handlerSaw": null,
- "received": "",
- "serverClosed": true,
+ "handlerSaw": "CONNECT h.test:80",
+ "received":
+ "HTTP/1.1 200 OK
+ content-type: text/plain;charset=utf-8
+ Date: Wed, 15 Jul 2026 19:14:30 GMT
+ Content-Length: 22
+
+ hello from the handler"
+ ,
+ "serverClosed": false,
}
- Expected - 3
+ Received
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (73281ae9d)
test/js/bun/http/bun-serve-connect.test.ts:
(pass) Bun.serve CONNECT > fetch handler never sees CONNECT; socket is closed with no response [7.05ms]
(pass) Bun.serve CONNECT > routes catch-all handler never sees CONNECT [1.49ms]
(pass) Bun.serve CONNECT > routes-only server (no fetch handler) closes on CONNECT [0.96ms]
(pass) Bun.serve CONNECT > pipelined bytes after CONNECT are discarded, not parsed as HTTP [1.01ms]
4 pass
0 fail
6 expect() calls
Ran 4 tests across 1 file. [175.00ms]
__F:0:S:0
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-connect.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (73281ae9d)
test/js/bun/http/bun-serve-connect.test.ts:
(pass) Bun.serve CONNECT > fetch handler never sees CONNECT; socket is closed with no response [410.17ms]
(pass) Bun.serve CONNECT > routes catch-all handler never sees CONNECT [64.05ms]
(pass) Bun.serve CONNECT > routes-only server (no fetch handler) closes on CONNECT [42.41ms]
(pass) Bun.serve CONNECT > pipelined bytes after CONNECT are discarded, not parsed as HTTP [74.81ms]
4 pass
0 fail
6 expect() calls
Ran 4 tests across 1 file. [3.00s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 929ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/13] cxx obj/unified/UnifiedSource-src_jsc_bindings-1.cpp.o
[2/13] cxx obj/unified/UnifiedSource-src_uws_sys-0.cpp.o
[3/13] cxx obj/unified/UnifiedSource-src_jsc_bindings-5.cpp.o
[4/13] cxx obj/unified/UnifiedSource-src_jsc_bindings_node-0.cpp.o
[5/13] cxx obj/src/jsc/bindings/bindings.cpp.o
[6/13] cxx obj/unified/UnifiedSource-src_jsc_bindings-3.cpp.o
[7/13] cxx obj/unified/UnifiedSource-src_jsc_bindings-4.cpp.o
[8/13] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[9/13] gen cpp.rs (cppbind)
[9/13] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update o
... (truncated)
diff hotspot
packages/bun-uws/src/App.h | 5 ++
packages/bun-uws/src/HttpContext.h | 8 ++
packages/bun-uws/src/HttpContextData.h | 1 +
src/runtime/server/mod.rs | 5 ++
src/uws_sys/App.rs | 5 ++
src/uws_sys/libuwsockets.cpp | 9 +++
test/js/bun/http/bun-serve-connect.test.ts | 117 +++++++++++++++++++++++++++++
7 files changed, 150 insertions(+)
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file
file reads edits tests
packages/bun-uws/src/App.h 1 1 0
packages/bun-uws/src/HttpContext.h 3 1 0
packages/bun-uws/src/HttpContextData.h 1 1 0
src/runtime/server/mod.rs 8 2 0
src/uws_sys/App.rs 2 2 0
src/uws_sys/libuwsockets.cpp 1 1 0
test/js/bun/http/bun-serve-connect.test.ts 1 8 0