Skip to content

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

Open
@futpib

Description

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
// 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.

node/lib/net.js

Line 405 in 6432060

options.autoDestroy = true;

$ 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    netIssues and PRs related to the net subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions