diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index 6ffcaaf6ad..db9cbf1757 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -901,6 +901,9 @@ const NodeHTTPServerSocket = class Socket extends Duplex { req.destroy(); } } + + // Emit close event on the socket to trigger onServerResponseClose + callCloseCallback(this); } #onCloseForDestroy(closeCallback) { this.#onClose(); diff --git a/test/regression/issue/14697.test.ts b/test/regression/issue/14697.test.ts new file mode 100644 index 0000000000..f14c3119a0 --- /dev/null +++ b/test/regression/issue/14697.test.ts @@ -0,0 +1,58 @@ +// https://github.com/oven-sh/bun/issues/14697 +// node:http ServerResponse should emit close event when client disconnects +import { expect, test } from "bun:test"; +import { createServer } from "node:http"; + +test("ServerResponse emits close event when client disconnects", async () => { + const events: string[] = []; + let resolveTest: () => void; + const testPromise = new Promise(resolve => { + resolveTest = resolve; + }); + + const server = createServer((req, res) => { + req.once("close", () => { + events.push("request-close"); + }); + + res.once("close", () => { + events.push("response-close"); + // Both events should have fired by now + resolveTest(); + }); + + // Don't send any response, let the client disconnect + }); + + await new Promise(resolve => { + server.listen(0, () => resolve()); + }); + + const port = (server.address() as any).port; + + // Connect and immediately disconnect + const socket = await Bun.connect({ + hostname: "localhost", + port, + socket: { + open(socket) { + // Send a minimal HTTP request + socket.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + // Immediately close the connection + socket.end(); + }, + data() {}, + error() {}, + close() {}, + }, + }); + + // Wait for both close events to fire + await testPromise; + + server.close(); + + // Both request and response should have emitted close events + expect(events).toContain("request-close"); + expect(events).toContain("response-close"); +});