From 717c76a8be74bd1798d7358d30a75f8b739a8668 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:47:48 +0000 Subject: [PATCH 1/4] node:tls: handle destroy() on a TLSSocket wrapping an unconnected stream new tls.TLSSocket(new net.Socket()).destroy() threw an uncaught 'handle.close is not a function' from closeSocketHandle. A client-side TLSSocket wrapping a Duplex that has not yet connected stores the stream itself as _handle (there is no native handle yet). Node wraps that stream in a JSStreamSocket whose close() destroys it; do the equivalent in closeSocketHandle by falling back to handle.destroy() when the handle has no close method. --- src/js/node/net.ts | 9 ++++++++- test/js/node/tls/node-tls-connect.test.ts | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 03ad37679853..157a908d89c6 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -4127,7 +4127,14 @@ function closeSocketHandle(self, isException, isCleanupPending = false) { const handle = self._handle; $debug("closeSocketHandle", isException, isCleanupPending, !!handle); if (handle) { - handle.close(onSocketHandleClosed); + // A client TLSSocket wrapping a not-yet-connected Duplex stores that stream + // as _handle directly (tls.ts sets `this._handle = socket`); Node wraps it + // in a JSStreamSocket whose close() destroys the stream. Do the same here. + if (typeof handle.close === "function") { + handle.close(onSocketHandleClosed); + } else if (!handle.destroyed) { + handle.destroy?.(); + } setImmediate(() => { $debug("emit close", isCleanupPending); self.emit("close", isException); diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 4050b066e815..618f2e3f8e3f 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -208,6 +208,25 @@ it("should be able to grab the JSStreamSocket constructor", () => { expect(socket._handle._parentWrap).not.toBeNull(); //@ts-ignore expect(socket._handle._parentWrap.constructor).toBeFunction(); + socket.destroy(); +}); + +// new tls.TLSSocket(rawSocket).destroy() before any connect: _handle is the +// wrapped Duplex itself (no native handle yet). Node's JSStreamSocket close() +// destroys that stream; the wrapper must too, and must not throw. +describe.each([ + ["net.Socket", () => new net.Socket()], + ["Duplex", () => new Duplex({ read() {}, write(_c, _e, cb) { cb(); } })], +])("new TLSSocket(%s).destroy() before connect", (_, makeRaw) => { + it("destroys both sockets and emits 'close'", async () => { + const raw = makeRaw(); + const socket = new tls.TLSSocket(raw); + const tlsClose = once(socket, "close"); + const rawClose = once(raw, "close"); + socket.destroy(); + await Promise.all([tlsClose, rawClose]); + expect({ tls: socket.destroyed, raw: raw.destroyed }).toEqual({ tls: true, raw: true }); + }); }); for (const { name, connect } of tests) { describe(name, () => { @@ -938,14 +957,13 @@ it("TLSSocket._requestCert follows Node's _init rule", () => { // Clients always request the peer certificate; servers only when asked. // Must be decided in the constructor, before a server wrap starts its // upgrade: https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L845-L848 - // Like the JSStreamSocket test above, the detached wrappers are not - // destroyed: tearing down a never-connected duplex wrap is its own quirk. const cases = [ new TLSSocket(new stream.PassThrough()), // client new TLSSocket(new stream.PassThrough(), { isServer: true }), new TLSSocket(new stream.PassThrough(), { isServer: true, requestCert: true }), ]; expect(cases.map(s => (s as any)._requestCert)).toEqual([true, false, true]); + for (const s of cases) s.destroy(); }); it("socket.ssl is assignable like Node's plain own property", async () => { From 55a41d56edc9014cfa3754e4124d8d2c4425ea48 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:50:06 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- test/js/node/tls/node-tls-connect.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 618f2e3f8e3f..cefaee966b61 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -216,7 +216,16 @@ it("should be able to grab the JSStreamSocket constructor", () => { // destroys that stream; the wrapper must too, and must not throw. describe.each([ ["net.Socket", () => new net.Socket()], - ["Duplex", () => new Duplex({ read() {}, write(_c, _e, cb) { cb(); } })], + [ + "Duplex", + () => + new Duplex({ + read() {}, + write(_c, _e, cb) { + cb(); + }, + }), + ], ])("new TLSSocket(%s).destroy() before connect", (_, makeRaw) => { it("destroys both sockets and emits 'close'", async () => { const raw = makeRaw(); From 76ed5b60d3cee0cd269136f807f9753909630513 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:53:42 +0000 Subject: [PATCH 3/4] tighten closeSocketHandle comment --- src/js/node/net.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 157a908d89c6..b05de984af16 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -4127,12 +4127,10 @@ function closeSocketHandle(self, isException, isCleanupPending = false) { const handle = self._handle; $debug("closeSocketHandle", isException, isCleanupPending, !!handle); if (handle) { - // A client TLSSocket wrapping a not-yet-connected Duplex stores that stream - // as _handle directly (tls.ts sets `this._handle = socket`); Node wraps it - // in a JSStreamSocket whose close() destroys the stream. Do the same here. if (typeof handle.close === "function") { handle.close(onSocketHandleClosed); } else if (!handle.destroyed) { + // tls.ts stores the wrapped Duplex directly as _handle; it has destroy(), not close(). handle.destroy?.(); } setImmediate(() => { From bd462795dbd40eb67ce0d4f43b7a00c327cd5510 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:56:50 +0000 Subject: [PATCH 4/4] ci: retrigger