nodejs / node

Node.js JavaScript runtime βœ¨πŸ’πŸš€βœ¨
https://nodejs.org
Other
106.62k stars 29.07k forks source link

`allowHalfOpen` option from `net` has no effect due to `autoDestroy = true` #49073

Open futpib opened 1 year ago

futpib commented 1 year ago

Version

v20.5.0 and v18.17.0 and more

Platform

Linux futpib-desktop 6.1.39-3-lts #1 SMP PREEMPT_DYNAMIC Wed, 02 Aug 2023 10:12:55 +0000 x86_64 GNU/Linux

Subsystem

net

What steps will reproduce the bug?

// net-allowHalfOpen.js

const net = require('net');

async function main() {
    let resolveServerSocket;
    const serverSocketPromise = new Promise((resolve, reject) => {
        resolveServerSocket = resolve;
    });

    const server = net.createServer({
        allowHalfOpen: true,
    }, (socket) => {
        resolveServerSocket(socket);
    }).listen();

    const clientSocket = await new Promise(resolve => {
        const socket = net.createConnection({
            allowHalfOpen: true,
            port: server.address().port,
            host: server.address().address,
        }, () => {
            resolve(socket);
        });
    });

    const serverSocket = await serverSocketPromise;

    await new Promise((resolve, reject) => {
        clientSocket.write('data written to client socket', (error) => {
            if (error) {
                reject(error);
            } else {
                resolve();
            }
        });
    });

    await new Promise(resolve => {
        clientSocket.end(resolve);
    });

    for await (const chunk of serverSocket) {
        console.log('read from server socket:', chunk.toString());
    }

    console.log('server socket ended');

    if (serverSocket.destroyed) {
        console.error('server socket is already destroyed 😿');
    }

    await new Promise((resolve, reject) => {
        serverSocket.write('data written to server socket', (error) => {
            if (error) {
                reject(error);
            } else {
                resolve();
            }
        });
    });

    await new Promise(resolve => {
        serverSocket.end(resolve);
    });

    for await (const chunk of clientSocket) {
        console.log('read from client socket:', chunk.toString());
    }

    console.log('client socket ended');

    server.close();
}

main();
$ node net-allowHalfOpen.js

read from server socket: data written to client socket
server socket ended
server socket is already destroyed 😿
node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
    at new NodeError (node:internal/errors:405:5)
    at _write (node:internal/streams/writable:331:11)
    at Writable.write (node:internal/streams/writable:344:10)
    at /home/futpib/code/tmp/net-allowHalfOpen.js:52:22
    at new Promise (<anonymous>)
    at main (/home/futpib/code/tmp/net-allowHalfOpen.js:51:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'ERR_STREAM_DESTROYED'
}

Node.js v20.5.0

How often does it reproduce? Is there a required condition?

Reproduces every time.

What is the expected behavior? Why is that the expected behavior?

Patch the example above with this workaround/hack to get what I consider to be expected behaviour:

--- net-allowHalfOpen.js        2023-08-09 00:52:47.771904675 +0400
+++ net-allowHalfOpen-hack.js   2023-08-09 00:52:42.848526177 +0400
@@ -24,6 +24,11 @@

     const serverSocket = await serverSocketPromise;

+    clientSocket._writableState.autoDestroy = false;
+    clientSocket._readableState.autoDestroy = false;
+    serverSocket._writableState.autoDestroy = false;
+    serverSocket._readableState.autoDestroy = false;
+
     await new Promise((resolve, reject) => {
         clientSocket.write('data written to client socket', (error) => {
             if (error) {
Full file after the hack ```js // net-allowHalfOpen-hack.js const net = require('net'); async function main() { let resolveServerSocket; const serverSocketPromise = new Promise((resolve, reject) => { resolveServerSocket = resolve; }); const server = net.createServer({ allowHalfOpen: true, }, (socket) => { resolveServerSocket(socket); }).listen(); const clientSocket = await new Promise(resolve => { const socket = net.createConnection({ allowHalfOpen: true, port: server.address().port, host: server.address().address, }, () => { resolve(socket); }); }); const serverSocket = await serverSocketPromise; clientSocket._writableState.autoDestroy = false; clientSocket._readableState.autoDestroy = false; serverSocket._writableState.autoDestroy = false; serverSocket._readableState.autoDestroy = false; await new Promise((resolve, reject) => { clientSocket.write('data written to client socket', (error) => { if (error) { reject(error); } else { resolve(); } }); }); await new Promise(resolve => { clientSocket.end(resolve); }); for await (const chunk of serverSocket) { console.log('read from server socket:', chunk.toString()); } console.log('server socket ended'); if (serverSocket.destroyed) { console.error('server socket is already destroyed 😿'); } await new Promise((resolve, reject) => { serverSocket.write('data written to server socket', (error) => { if (error) { reject(error); } else { resolve(); } }); }); await new Promise(resolve => { serverSocket.end(resolve); }); for await (const chunk of clientSocket) { console.log('read from client socket:', chunk.toString()); } console.log('client socket ended'); server.close(); } main(); ```
$ node net-allowHalfOpen-hack.js

read from server socket: data written to client socket
server socket ended
read from client socket: data written to server socket
client socket ended

This is the expected behavior because this allows for half-open sockets to actually be used, otherwise there is no point in having a allowHalfOpen option, it just does not work. More precisely, the socket is closed (destroyed) when only one of it's directions has ended.

What do you see instead?

The server socket is closed regardless of allowHalfOpen option due to autoDestroy option hard set to true.

https://github.com/nodejs/node/blob/6432060c7ce1670467c230d2c0c925465dd311f8/lib/net.js#L405

$ node net-allowHalfOpen.js

read from server socket: data written to client socket
server socket ended
server socket is already destroyed 😿
node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
    at new NodeError (node:internal/errors:405:5)
    at _write (node:internal/streams/writable:331:11)
    at Writable.write (node:internal/streams/writable:344:10)
    at /home/futpib/code/tmp/net-allowHalfOpen.js:52:22
    at new Promise (<anonymous>)
    at main (/home/futpib/code/tmp/net-allowHalfOpen.js:51:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'ERR_STREAM_DESTROYED'
}

Node.js v20.5.0

Additional information

No response

TonyBogdanov commented 7 months ago

I'm also experiencing this bug.

jakecastelli commented 3 months ago

I will take a look πŸ‘€