diff --git a/src/js/internal/http1_server_fallback.ts b/src/js/internal/http1_server_fallback.ts index 5a19fd5152fd..398ff1a9fa89 100644 --- a/src/js/internal/http1_server_fallback.ts +++ b/src/js/internal/http1_server_fallback.ts @@ -521,6 +521,10 @@ function connectionListenerHTTP1(server, socket, options) { parser.close(); } catch {} }); + // Node's HTTP parser consumes an injected socket's native handle directly, + // so a pause from its previous owner does not prevent request parsing. The + // JS fallback reads through the stream and must resume it explicitly. + socket.resume(); } function closeIdleHttp1Connections(server) { diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 88f1647cd00a..ab8cbd0e9588 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -2607,6 +2607,10 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection, connection.on("drain", events[2]); connection.on("close", events[3]); this._handle = result; + // Node starts the TLSWrap read side even when the injected transport was + // paused by its previous owner. The TLS handshake must not inherit that + // application-level pause. + this.read(0); this.emit(kUpgradeAttached); return; } @@ -2633,6 +2637,9 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection, this.once("end", this[kCloseRawConnection]); raw.connecting = false; this._handle = tlsHandle; + // Match Node's initRead(): an injected socket may be paused, but TLS still + // needs to consume the ClientHello before exposing its readable stream. + this.read(0); this.emit(kUpgradeAttached); }); }; diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 62d22087588d..8ef3dadac976 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -4510,6 +4510,40 @@ it("connectionListener hands off Upgrade and CONNECT like Node", async () => { } }); +it("connectionListener consumes an injected socket paused by its previous owner", async () => { + const server = createServer((_req, res) => res.end("injected-ok")); + const front = createNetServer(socket => { + socket.pause(); + server.emit("connection", socket); + }); + + try { + await once(front.listen(0, "127.0.0.1"), "listening"); + const response = await new Promise<{ statusCode: number | undefined; body: string }>((resolve, reject) => { + const request = get( + { + host: "127.0.0.1", + port: (front.address() as AddressInfo).port, + agent: false, + }, + response => { + const chunks: Buffer[] = []; + response.on("data", chunk => chunks.push(chunk)); + response.on("end", () => + resolve({ statusCode: response.statusCode, body: Buffer.concat(chunks).toString("utf8") }), + ); + }, + ); + request.on("error", reject); + }); + + expect(response).toEqual({ statusCode: 200, body: "injected-ok" }); + } finally { + front.close(); + server.close(); + } +}); + it("https wraps a raw socket injected through the connection event", async () => { const server = createHttpsServer(tlsCert, (req, res) => { expect((req.socket as any).encrypted).toBe(true); @@ -4519,6 +4553,7 @@ it("https wraps a raw socket injected through the connection event", async () => const rawClosed = Promise.withResolvers(); const front = createNetServer(socket => { socket.once("close", () => rawClosed.resolve()); + socket.pause(); server.emit("connection", socket); });