Bun.serve: close the connection after responding to a CONNECT request

RO
3mo ago
+126-0

Repro

const server = Bun.serve({ port: 0, fetch: () => new Response("nope", { status: 404 }) });
const sock = await Bun.connect({
  hostname: "127.0.0.1", port: server.port,
  socket: {
    data(_, d) { console.log(JSON.stringify(d.toString().split("\r\n")[0])); },
    close() { console.log("closed"); },
  },
});
sock.write("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n");
// before: "HTTP/1.1 404 Not Found", then nothing: the connection stays open and
//         anything written to it afterwards (a pipelined GET, tunnel bytes) is
//         discarded without a response
// after:  "HTTP/1.1 404 Not Found", "closed"

Cause

HttpParser.h sets the per-connection isConnectRequest flag when it parses a CONNECT request-line and from then on stops parsing the connection as HTTP: after the dispatch it marks the rest of the buffer consumed, and every later read goes straight to the data handler (consumePostPadded), which on a Bun.serve connection has no inStream to deliver to. node:http relies on this to hand the connection to its 'connect' listener as a tunnel. Bun.serve has nothing to hand a tunnel to: the request went through the normal handler (fetch, routes, or the 404 fallback), the response was written, and the connection was then kept alive in tunnel mode as a sink.

Fix

In the uWS request dispatch (HttpContext.h), when a CONNECT is dispatched on a Bun.serve context (!IsNodeHttp), set HTTP_CONNECTION_CLOSE on the connection, the same state an HTTP/1.0 or Connection: close request sets a few lines above. The handler still answers however it likes, and the existing close gates (post-parse in onData, internalEnd, onWritable) close the socket once that response has been flushed. Every Bun.serve response path already feeds this state into its end() call, so this covers fetch, routes, static/file routes and the no-handler 404 alike. The node:http instantiation is unchanged (its CONNECT handling lives in _http_server.ts: 'connect' listener, or close when there is none).

As with the other close-marked requests on Bun.serve, no Connection: close header is added; the response is complete and the socket is closed behind it.

#34261 addresses the same report differently, by dropping CONNECT before dispatch with no response written. This PR keeps the response and closes afterwards.

Verification

test/js/bun/http/bun-server.test.ts, new CONNECT requests block. Each case sends a CONNECT head followed by a pipelined GET on a server with a long idle timeout and waits for the server to end the connection, then checks that exactly one response (the handler's) came back:

  • head in one read, and head split across two reads (exercises the fallback buffer path)
  • handler that responds from a later event loop turn (close happens outside the parse window)
  • routes-only server with no fetch (404 fallback)

Without the change all four time out waiting for the close (the 404/405 arrives, the connection stays open); with it they pass. test/js/node/http/node-http-connect.test.ts and the node CONNECT regression tests (25862, 32734) still pass.


no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/http/bun-server.test.ts