diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 14302e78b572..54e118ff90be 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -3891,9 +3891,11 @@ class ServerHttp2Session extends Http2Session { } get socket() { + // Once the underlying socket is detached (destroy/close), Node returns + // undefined instead of the cached proxy so `instanceof` and truthiness + // guards in cleanup handlers stay inert. + if (!this[bunHTTP2Socket]) return undefined; if (this.#socket_proxy) return this.#socket_proxy; - const socket = this[bunHTTP2Socket]; - if (!socket) return null; this.#socket_proxy = new Proxy(this, proxySocketHandler); return this.#socket_proxy; } @@ -4646,9 +4648,11 @@ class ClientHttp2Session extends Http2Session { return this.#parser?.setLocalWindowSize?.(windowSize); } get socket() { + // Once the underlying socket is detached (destroy/close), Node returns + // undefined instead of the cached proxy so `instanceof` and truthiness + // guards in cleanup handlers stay inert. + if (!this[bunHTTP2Socket]) return undefined; if (this.#socket_proxy) return this.#socket_proxy; - const socket = this[bunHTTP2Socket]; - if (!socket) return null; this.#socket_proxy = new Proxy(this, proxySocketHandler); return this.#socket_proxy; } diff --git a/test/js/node/http2/node-http2.test.js b/test/js/node/http2/node-http2.test.js index 8f658be0d0f6..c4c2c78f7a86 100644 --- a/test/js/node/http2/node-http2.test.js +++ b/test/js/node/http2/node-http2.test.js @@ -1617,6 +1617,47 @@ it("sensitive headers should work", async () => { } }); +it("session.socket is undefined after the session is destroyed", async () => { + const server = http2.createServer(); + try { + server.on("session", ss => ss.on("error", () => {})); + server.on("stream", stream => { + stream.on("error", () => {}); + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + const listening = Promise.withResolvers(); + server.on("error", listening.reject); + server.listen(0, "127.0.0.1", listening.resolve); + await listening.promise; + + const client = http2.connect(`http://127.0.0.1:${server.address().port}`); + const connected = Promise.withResolvers(); + client.on("error", connected.reject); + client.on("connect", connected.resolve); + await connected.promise; + + // Cache the proxy before destroy so we exercise the stale-proxy path; + // operations on the captured reference must still throw SOCKET_UNBOUND. + const socketBefore = client.socket; + expect(socketBefore).toBeDefined(); + + const closed = Promise.withResolvers(); + client.on("close", closed.resolve); + client.destroy(); + await closed.promise; + + expect(client.socket).toBeUndefined(); + expect(() => client.socket instanceof net.Socket).not.toThrow(); + expect(client.socket instanceof net.Socket).toBe(false); + expect(() => socketBefore instanceof net.Socket).toThrow( + expect.objectContaining({ code: "ERR_HTTP2_SOCKET_UNBOUND" }), + ); + } finally { + server.close(); + } +}); + it("http2 session.goaway() validates input types", async done => { const { mustCall } = createCallCheckCtx(done); const server = http2.createServer((req, res) => {