From 0539c7f10507b7abef9782d2cc2c2b80a0b3d3d4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 11 Sep 2026 16:51:20 +0200 Subject: [PATCH] stream: destroy half-open sockets after iteration Async iteration left the writable side of `allowHalfOpen` duplexes open after clean EOF. For `net.Socket` instances, this retained a live handle after the peer disconnected. Restore default iterator destruction. Callers that need to continue writing can use `iterator({ destroyOnReturn: false })`. Assisted-by: Codex Signed-off-by: Matteo Collina --- lib/internal/streams/readable.js | 9 +--- ...-net-allow-half-open-async-iter-destroy.js | 53 +++++++++++++++++++ .../test-net-allow-half-open-async-iter.js | 2 +- ...am-readable-async-iter-half-open-duplex.js | 2 +- 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-net-allow-half-open-async-iter-destroy.js diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 847b3837af4c..72547d8c9f7f 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -1453,16 +1453,9 @@ function createAsyncIterator(stream, options) { function finalize() { completed = true; - const preserveHalfOpenDuplex = - error === null && - stream.allowHalfOpen === true && - stream.writable === true && - stream.writableEnded !== true; - if ( (error || options?.destroyOnReturn !== false) && - (error === undefined || stream._readableState.autoDestroy) && - !preserveHalfOpenDuplex + (error === undefined || stream._readableState.autoDestroy) ) { destroyImpl.destroyer(stream, null); } else { diff --git a/test/parallel/test-net-allow-half-open-async-iter-destroy.js b/test/parallel/test-net-allow-half-open-async-iter-destroy.js new file mode 100644 index 000000000000..9e19eab50684 --- /dev/null +++ b/test/parallel/test-net-allow-half-open-async-iter-destroy.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +(async function() { + let resolveServerSocket; + const serverSocketPromise = new Promise((resolve) => { + resolveServerSocket = resolve; + }); + + const server = net.createServer({ + allowHalfOpen: true, + }, common.mustCall((socket) => { + resolveServerSocket(socket); + })); + + server.on('error', common.mustNotCall()); + server.on('close', common.mustCall()); + + await new Promise((resolve) => { + server.listen(0, common.localhostIPv4, resolve); + }); + + const clientSocket = net.createConnection({ + port: server.address().port, + host: server.address().address, + }); + clientSocket.on('error', common.mustNotCall()); + + await new Promise((resolve) => { + clientSocket.end('data', resolve); + }); + + const serverSocket = await serverSocketPromise; + + let serverRead = ''; + for await (const chunk of serverSocket) { + serverRead += chunk; + } + + const destroyed = serverSocket.destroyed; + serverSocket.destroy(); + clientSocket.destroy(); + + await new Promise((resolve) => { + server.close(resolve); + }); + + assert.strictEqual(serverRead, 'data'); + assert.strictEqual(destroyed, true); +})().then(common.mustCall()); diff --git a/test/parallel/test-net-allow-half-open-async-iter.js b/test/parallel/test-net-allow-half-open-async-iter.js index 30d82fc4e5b3..4976d407612b 100644 --- a/test/parallel/test-net-allow-half-open-async-iter.js +++ b/test/parallel/test-net-allow-half-open-async-iter.js @@ -49,7 +49,7 @@ const net = require('net'); }); let serverRead = ''; - for await (const chunk of serverSocket) { + for await (const chunk of serverSocket.iterator({ destroyOnReturn: false })) { serverRead += chunk; } diff --git a/test/parallel/test-stream-readable-async-iter-half-open-duplex.js b/test/parallel/test-stream-readable-async-iter-half-open-duplex.js index 13e0d4b2f3df..61e5333daf6e 100644 --- a/test/parallel/test-stream-readable-async-iter-half-open-duplex.js +++ b/test/parallel/test-stream-readable-async-iter-half-open-duplex.js @@ -26,7 +26,7 @@ const { (async () => { let read = ''; - for await (const chunk of duplex) { + for await (const chunk of duplex.iterator({ destroyOnReturn: false })) { read += chunk; }