From fa57b72804d8cf29130c987b83048d88ce8ce0e8 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 11 Jul 2026 18:01:19 +0200 Subject: [PATCH] http2: don't throw when destroying socket proxy Calling destroy() on the http2session.socket proxy now destroys the session instead of throwing ERR_HTTP2_NO_SOCKET_MANIPULATION. Refs: https://github.com/nodejs/undici/issues/5525 Signed-off-by: Matteo Collina --- doc/api/http2.md | 10 +++- lib/internal/http2/core.js | 4 +- .../test-http2-socket-proxy-destroy.js | 57 +++++++++++++++++++ test/parallel/test-http2-socket-proxy.js | 4 +- 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-http2-socket-proxy-destroy.js diff --git a/doc/api/http2.md b/doc/api/http2.md index 1003fb3fb0e90e..0b5d665dbc2b71 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -722,6 +722,11 @@ registered as a listener on the `'timeout'` event. * Type: {net.Socket|tls.TLSSocket} @@ -729,11 +734,12 @@ added: v8.4.0 Returns a `Proxy` object that acts as a `net.Socket` (or `tls.TLSSocket`) but limits available methods to ones safe to use with HTTP/2. -`destroy`, `emit`, `end`, `pause`, `read`, `resume`, and `write` will throw +`emit`, `end`, `pause`, `read`, `resume`, and `write` will throw an error with code `ERR_HTTP2_NO_SOCKET_MANIPULATION`. See [`Http2Session` and Sockets][] for more information. -`setTimeout` method will be called on this `Http2Session`. +`destroy`, `setTimeout`, `ref`, and `unref` methods will be called on this +`Http2Session`. All other interactions will be routed directly to the socket. diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 0691b4aabfc81b..4a0ba7330e030a 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -984,8 +984,8 @@ const proxySocketHandler = { case 'setTimeout': case 'ref': case 'unref': - return session[prop].bind(session); case 'destroy': + return session[prop].bind(session); case 'emit': case 'end': case 'pause': @@ -1018,9 +1018,9 @@ const proxySocketHandler = { case 'setTimeout': case 'ref': case 'unref': + case 'destroy': session[prop] = value; return true; - case 'destroy': case 'emit': case 'end': case 'pause': diff --git a/test/parallel/test-http2-socket-proxy-destroy.js b/test/parallel/test-http2-socket-proxy-destroy.js new file mode 100644 index 00000000000000..c98b2e80ee0b9c --- /dev/null +++ b/test/parallel/test-http2-socket-proxy-destroy.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Calling .destroy() on the socket proxy must not throw +// (previously threw ERR_HTTP2_NO_SOCKET_MANIPULATION) and must +// destroy the Http2Session and the underlying socket. + +const server = h2.createServer(); + +server.on('stream', common.mustCall(function(stream) { + stream.respond(); + stream.end('ok'); +}, 2)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + // Test destroy() without an error. + { + const client = h2.connect(`http://localhost:${port}`); + const request = client.request(); + request.resume(); + request.on('close', common.mustCall(() => { + const socket = client.socket; + socket.destroy(); + assert.strictEqual(client.destroyed, true); + client.on('close', common.mustCall(() => { + assert.strictEqual(client.socket, undefined); + // Destroying again through a stale proxy reference must not throw. + socket.destroy(); + })); + })); + client.on('error', common.mustNotCall()); + } + + // Test destroy() with an error. + { + const client = h2.connect(`http://localhost:${port}`); + const request = client.request(); + request.resume(); + request.on('close', common.mustCall(() => { + client.socket.destroy(new Error('boom')); + assert.strictEqual(client.destroyed, true); + })); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.message, 'boom'); + })); + client.on('close', common.mustCall(() => { + server.close(); + })); + } +})); diff --git a/test/parallel/test-http2-socket-proxy.js b/test/parallel/test-http2-socket-proxy.js index 3294cf7ccb9011..37f294742a28b0 100644 --- a/test/parallel/test-http2-socket-proxy.js +++ b/test/parallel/test-http2-socket-proxy.js @@ -52,7 +52,8 @@ server.on('stream', common.mustCall(function(stream, headers) { assert(inspectedTimersList.includes(' _idleNext: [Timeout]')); assert(!inspectedTimersList.includes(' _idleNext: [Timeout]')); - assert.throws(() => socket.destroy, errMsg); + assert.strictEqual(typeof socket.destroy, 'function'); + assert.throws(() => socket.emit, errMsg); assert.throws(() => socket.end, errMsg); assert.throws(() => socket.pause, errMsg); @@ -63,7 +64,6 @@ server.on('stream', common.mustCall(function(stream, headers) { assert.throws(() => socket.setKeepAlive, errMsg); assert.throws(() => socket.setNoDelay, errMsg); - assert.throws(() => (socket.destroy = undefined), errMsg); assert.throws(() => (socket.emit = undefined), errMsg); assert.throws(() => (socket.end = undefined), errMsg); assert.throws(() => (socket.pause = undefined), errMsg);