From 4603836db97994ba8f95b8bc5ef2976c98392e79 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:54:09 +0000 Subject: [PATCH] node:net: remove the listen({ signal }) abort listener when the server closes --- src/js/node/net.ts | 5 ++++- test/js/node/net/node-net-server.test.ts | 27 +++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 58e05eb224ee..18ec309c2c4d 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -3997,7 +3997,10 @@ function addServerAbortSignalOption(self, options) { if (signal.aborted) { process.nextTick(onAborted); } else { - signal.addEventListener("abort", onAborted); + // Detached on 'close', otherwise a long-lived signal retains every server that ever listened with it. + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(signal, onAborted); + self.once("close", disposable[Symbol.dispose]); } } diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 2fb444c2e07e..ef5d73112446 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -2,7 +2,7 @@ import { realpathSync } from "fs"; import { bunEnv, bunExe, tempDir } from "harness"; import { AddressInfo, createServer, Server, Socket } from "net"; import { createTest } from "node-harness"; -import { once } from "node:events"; +import { getEventListeners, once } from "node:events"; import { tmpdir } from "os"; import { join } from "path"; @@ -433,6 +433,31 @@ describe("net.createServer events", () => { }); }); + it("listen({ signal }) removes its abort listener from the signal once the server closes", async () => { + const controller = new AbortController(); + const { signal } = controller; + const abortListenerCounts: number[] = []; + let closeEvents = 0; + + for (let i = 0; i < 3; i++) { + const server = createServer(); + server.on("close", () => closeEvents++); + server.listen({ port: 0, host: "127.0.0.1", signal }); + await once(server, "listening"); + abortListenerCounts.push(getEventListeners(signal, "abort").length); + server.close(); + await once(server, "close"); + abortListenerCounts.push(getEventListeners(signal, "abort").length); + } + expect(abortListenerCounts).toEqual([1, 0, 1, 0, 1, 0]); + + // A stale listener would call close() on the already-closed server again, + // which emits a second 'close' on the next tick. + controller.abort(); + await new Promise(resolve => process.nextTick(resolve)); + expect(closeEvents).toBe(3); + }); + it("should echo data", done => { const { mustNotCall } = createCallCheckCtx(done); let timeout: Timer;