From 3bb5298d4b5bec8ba9e2f435e83da9e304abaca6 Mon Sep 17 00:00:00 2001 From: Amilcar Silvestre Date: Sat, 4 Jul 2026 09:59:00 -0400 Subject: [PATCH] fix(connect): make timeout cover the STOMP handshake The connect `timeout` only covered the transport (TCP/TLS) connect: it was cleared in onConnected (on transport connect) before the STOMP CONNECT/CONNECTED handshake ran, and that handshake had no timeout of its own (heartbeats are only armed after CONNECTED). A server that accepts the socket but never sends CONNECTED left the connection hung forever, with the socket open and the callback never invoked. Keep the timeout armed until the handshake settles (cleared in a wrapped connectListener). A stalled handshake now triggers client.destroy('connect timed out'), closing the socket and surfacing an error to the callback. timeout now bounds the whole connect (transport + handshake). Adds a regression test. Co-Authored-By: Claude --- lib/connect.js | 29 +++++++++++++++++++++-------- test/connect.js | 27 +++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/lib/connect.js b/lib/connect.js index a560fc1..204971c 100644 --- a/lib/connect.js +++ b/lib/connect.js @@ -50,21 +50,34 @@ function connect() { }; const onConnected = function() { - + if (originalSocketDestroy) { socket.destroy = originalSocketDestroy; } - - cleanup(); - + + // Keep the connect timeout armed through the STOMP handshake (do not + // clear it on transport connect). Otherwise a server that accepts the + // socket but never sends CONNECTED hangs forever with the socket open + // and the callback never fires. Cleared when the handshake settles. + client.removeListener('error', onError); + client.removeListener('connect', onConnected); + client.emit('socket-connect'); - + const connectOpts = Object.assign( - {host: options.host}, + {host: options.host}, options.connectHeaders ); - - client.connect(connectOpts, connectListener); + + client.connect(connectOpts, function(error, connectedClient) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + if (typeof connectListener === 'function') { + connectListener(error, connectedClient); + } + }); }; let transportConnect = net.connect; diff --git a/test/connect.js b/test/connect.js index 78e7ed0..0e3a054 100644 --- a/test/connect.js +++ b/test/connect.js @@ -84,9 +84,9 @@ describe('connect(options, [connectionListener])', function(){ }); it('should callback on error', function(done){ - + var server = startBrokenServer(); - + connect({ host:'127.0.0.1', port: server.address().port @@ -95,6 +95,29 @@ describe('connect(options, [connectionListener])', function(){ done(); }); }); + + it('should time out when the STOMP handshake stalls after the transport connects', function(done){ + + // A server that accepts the TCP socket but never completes the STOMP handshake + // (never sends CONNECTED). Before the fix, the connect timeout was cleared once the + // transport connected, so this hung forever with the socket left open. + var sockets = []; + var server = net.createServer({family: 4}, function(socket){ + sockets.push(socket); // hold the socket open, never respond + }); + server.listen(0); + + connect({ + host: '127.0.0.1', + port: server.address().port, + timeout: 150 + }, function(error){ + assert(error, 'expected a connect timeout error'); + sockets.forEach(function(s){ s.destroy(); }); + server.close(); + done(); + }); + }); it('should accept a transport connect function', function(done){