From 598012a29651e29eacb33f74bbd2bcaa30a6994f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 28 Oct 2025 22:43:59 +0000 Subject: [PATCH] fix(node:http): emit close event on ServerResponse when client disconnects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14697 ServerResponse was not emitting the "close" event when the client disconnected. This was causing resource leaks in long-running connections like server-sent events and live streaming. The issue was in the NodeHTTPServerSocket's #onClose() method, which cleaned up the request but did not call the socket's close callback. This prevented onServerResponseClose from being invoked, which is responsible for emitting the close event on the ServerResponse. The fix adds a call to callCloseCallback(this) at the end of #onClose(), ensuring that the close event propagates properly to the ServerResponse. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/js/node/_http_server.ts | 3 ++ test/regression/issue/14697.test.ts | 58 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 test/regression/issue/14697.test.ts 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"); +});