From a3644a761b163fd902287bc92a9b0713e3007d12 Mon Sep 17 00:00:00 2001 From: Ian-Costa18 <10099038+Ian-Costa18@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:38:56 +0000 Subject: [PATCH] Reconnect after a SOCKS proxy connection failure On the SOCKS path connect() sets this.socket = null and hands the createConnection() rejection to onSocketError(), whose emit('error') is commented out. Socket handlers are bound in _onSocketCreate(), which only runs from the .then, so a rejection has nothing bound to it: no 'close' is emitted, Connection.socketClose() never runs, and no reconnect is ever scheduled. The connection sits in SOCK_CONNECTING with a null socket and no timer, indefinitely. The direct path assigns this.socket synchronously and calls _onSocketCreate() immediately, so the same class of failure emits 'close' and reconnects normally. Emit the close ourselves when there is no socket to emit it for us. The !this.socket guard leaves failures arriving through the bound 'error' handler alone, since those have a real socket that will emit its own 'close'. --- src/transports/net.js | 9 +++++++++ test/net.transport.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/transports/net.js b/src/transports/net.js index dcd7d786..54ebd475 100644 --- a/src/transports/net.js +++ b/src/transports/net.js @@ -212,6 +212,15 @@ module.exports = class Connection extends EventEmitter { this.debugOut('socketError() ' + err.message); this.last_socket_error = err; // this.emit('error', err); + + // With a SOCKS proxy there is no socket until createConnection() resolves, so a + // rejection here has nothing bound to it: no 'close' is emitted, Connection's + // socketClose() never runs, and no reconnect is ever scheduled. Emit the close + // ourselves so the normal backoff applies, as the direct path already gets from + // _onSocketCreate(). + if (!this.socket) { + this.onSocketClose(); + } } onSocketTimeout() { diff --git a/test/net.transport.test.js b/test/net.transport.test.js index 5daffe2c..34c2cc12 100644 --- a/test/net.transport.test.js +++ b/test/net.transport.test.js @@ -505,6 +505,34 @@ describe('src/transports/net.js', function() { assert.equal(conn.last_socket_error, err); }); + + it('should emit close when there is no socket', function() { + // The SOCKS path assigns this.socket only once createConnection() resolves, so a + // rejection arrives with nothing bound to emit 'close' for it. Without this the + // connection sits in SOCK_CONNECTING forever and never reconnects. + const conn = new Connection({}); + const spy = sinon.spy(); + const err = new Error('Proxy connection timed out'); + conn.on('close', spy); + conn.socket = null; + conn.onSocketError(err); + + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(err); + assert.equal(conn.state, 0); // SOCK_DISCONNECTED + }); + + it('should not emit close when a socket exists', function() { + // A bound socket emits its own 'close', so synthesising one here would schedule + // two reconnects for a single failure. + const conn = new Connection({}); + const spy = sinon.spy(); + conn.on('close', spy); + conn.socket = createMockSocket(); + conn.onSocketError(new Error('ECONNRESET')); + + expect(spy).to.not.have.been.called; + }); }); describe('onSocketTimeout()', function() {