Skip to content

[fix] Call the _final() callback if the socket is closed prematurely - #2345

Open
mkantautas wants to merge 1 commit into
websockets:masterfrom
mkantautas:fix/stream-final-socket-close
Open

mkantautas wants to merge 1 commit into
websockets:masterfrom
mkantautas:fix/stream-final-socket-close

Conversation

@mkantautas

Copy link
Copy Markdown

If the writable side of a stream returned by createWebSocketStream() is ended and the socket is destroyed before it emits 'finish', the _final() callback is never called. This happens when the other peer does not answer the close frame and the close timer destroys the socket, or when the connection is reset. The stream never emits 'finish' or 'close' and is never destroyed, so pipeline() never calls its callback.

const { pipeline, Readable } = require('stream');
const { WebSocket, WebSocketServer, createWebSocketStream } = require('ws');

const wss = new WebSocketServer({ port: 0 }, () => {
  const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
    closeTimeout: 1000
  });
  const duplex = createWebSocketStream(ws);

  duplex.resume();
  pipeline(Readable.from(['foo']), duplex, (err) => {
    console.log('pipeline callback', err); // Never called.
  });
});

wss.on('connection', (ws) => {
  ws.pause(); // Never answer the close frame.
});

This makes _final() also listen for the 'close' event of the socket. The listener is prepended so that 'finish' is still emitted before 'end' and the existing 'end' listener destroys the stream. Without that, the (2/2) test fails on Node.js >= 16. If the stream is being destroyed, the callback is not called, so pipeline() still reports the error passed to duplex.destroy().

#2338 is related. It handles the case where the socket is already destroyed when _final() is called, and does not fix this one, where the socket is destroyed while _final() waits for 'finish'.

If the socket was destroyed before it was finished, for example because
the closing handshake timed out or the connection was reset, the
callback of `duplex._final()` was never called. The `'finish'` and
`'close'` events were not emitted and the stream was never destroyed.
@lpinca

lpinca commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

As with #2338 once the Duplex has been created, you should work with it rather than the underlying socket. Cases were the closing handshake does not complete cleanly should be handled by finding inactive connections and calling duplex.destroy() on them. The stream should not be allowed to terminate cleanly and implicitly, as if everything had proceeded successfully (it did not finish).

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants