From 793f5e4a99db351d5354627b8484d1b1cf8b7e37 Mon Sep 17 00:00:00 2001 From: Yurii Bliuchak <1957659+bliuchak@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:28:23 +0100 Subject: [PATCH 1/2] feat: use pure async/await instead of nodeify --- src/anonymize_proxy.ts | 11 +- src/server.ts | 23 +--- src/tcp_tunnel_tools.ts | 9 +- src/utils/nodeify.ts | 16 --- test/anonymize_proxy.js | 185 ++++++++++------------------ test/anonymize_proxy_no_password.js | 38 ++---- test/ee-memory-leak.js | 28 ++--- test/server.js | 66 +++++----- test/socks.js | 90 ++++++-------- test/tcp_tunnel.js | 25 ++-- test/tools.js | 51 -------- 11 files changed, 183 insertions(+), 359 deletions(-) delete mode 100644 src/utils/nodeify.ts diff --git a/src/anonymize_proxy.ts b/src/anonymize_proxy.ts index 243c3c40..33fe190b 100644 --- a/src/anonymize_proxy.ts +++ b/src/anonymize_proxy.ts @@ -4,7 +4,6 @@ import type net from 'node:net'; import { URL } from 'node:url'; import { Server, SOCKS_PROTOCOLS } from './server.js'; -import { nodeify } from './utils/nodeify.js'; // Dictionary, key is value returned from anonymizeProxy(), value is Server instance. const anonymizedProxyUrlToServer: Record = {}; @@ -22,7 +21,6 @@ export interface AnonymizeProxyOptions { */ export const anonymizeProxy = async ( options: string | AnonymizeProxyOptions, - callback?: (error: Error | null) => void, ): Promise => { let proxyUrl: string; let port = 0; @@ -52,7 +50,7 @@ export const anonymizeProxy = async ( // If upstream proxy requires no password or if there is no need to ignore HTTPS proxy cert errors, return it directly if (!parsedProxyUrl.username && !parsedProxyUrl.password && (!ignoreProxyCertificate || parsedProxyUrl.protocol !== 'https:')) { - return nodeify(Promise.resolve(proxyUrl), callback); + return proxyUrl; } let server: Server & { port: number }; @@ -82,7 +80,7 @@ export const anonymizeProxy = async ( return url; }); - return nodeify(promise, callback); + return promise; }; /** @@ -94,7 +92,6 @@ export const anonymizeProxy = async ( export const closeAnonymizedProxy = async ( anonymizedProxyUrl: string, closeConnections: boolean, - callback?: (error: Error | null, result?: boolean) => void, ): Promise => { if (typeof anonymizedProxyUrl !== 'string') { throw new Error('The "anonymizedProxyUrl" parameter must be a string'); @@ -102,7 +99,7 @@ export const closeAnonymizedProxy = async ( const server = anonymizedProxyUrlToServer[anonymizedProxyUrl]; if (!server) { - return nodeify(Promise.resolve(false), callback); + return false; } delete anonymizedProxyUrlToServer[anonymizedProxyUrl]; @@ -110,7 +107,7 @@ export const closeAnonymizedProxy = async ( const promise = server.close(closeConnections).then(() => { return true; }); - return nodeify(promise, callback); + return promise; }; type Callback = ({ diff --git a/src/server.ts b/src/server.ts index b12b98c7..c7898653 100644 --- a/src/server.ts +++ b/src/server.ts @@ -22,7 +22,6 @@ import { RequestError } from './request_error.js'; import type { Socket, TLSSocket } from './socket.js'; import { badGatewayStatusCodes } from './statuses.js'; import { getTargetStats } from './utils/count_target_bytes.js'; -import { nodeify } from './utils/nodeify.js'; import { normalizeUrlPort } from './utils/normalize_url_port.js'; import { parseAuthorizationHeader } from './utils/parse_authorization_header.js'; import { redactUrl } from './utils/redact_url.js'; @@ -666,12 +665,10 @@ export class Server extends EventEmitter { /** * Starts listening at a port specified in the constructor. */ - async listen(callback?: (error: NodeJS.ErrnoException | null) => void): Promise { - const promise = new Promise((resolve, reject) => { - // Unfortunately server.listen() is not a normal function that fails on error, - // so we need this trickery + async listen(): Promise { + return new Promise((resolve, reject) => { const onError = (error: NodeJS.ErrnoException) => { - this.log(null, `Listen failed: ${error}`); + this.log(null, `Listen error: ${error}`); removeListeners(); reject(error); }; @@ -690,8 +687,6 @@ export class Server extends EventEmitter { this.server.on('listening', onListening); this.server.listen(this.port, this.host); }); - - return nodeify(promise, callback); } /** @@ -751,12 +746,7 @@ export class Server extends EventEmitter { * Closes the proxy server. * @param closeConnections If true, pending proxy connections are forcibly closed. */ - async close(closeConnections: boolean, callback?: (error: NodeJS.ErrnoException | null) => void): Promise { - if (typeof closeConnections === 'function') { - callback = closeConnections; - closeConnections = false; - } - + async close(closeConnections = false): Promise { if (closeConnections) { this.closeConnections(); } @@ -765,10 +755,7 @@ export class Server extends EventEmitter { const { server } = this; // @ts-expect-error Let's make sure we can't access the server anymore. this.server = null; - const promise = util.promisify(server.close).bind(server)(); - return nodeify(promise, callback); + await util.promisify(server.close).bind(server)(); } - - return nodeify(Promise.resolve(), callback); } } diff --git a/src/tcp_tunnel_tools.ts b/src/tcp_tunnel_tools.ts index 1e2cc566..3d429d0a 100644 --- a/src/tcp_tunnel_tools.ts +++ b/src/tcp_tunnel_tools.ts @@ -2,7 +2,6 @@ import net from 'node:net'; import { URL } from 'node:url'; import { chain } from './chain.js'; -import { nodeify } from './utils/nodeify.js'; const runningServers: Record }> = {}; @@ -23,7 +22,6 @@ export async function createTunnel( verbose?: boolean; ignoreProxyCertificate?: boolean; }, - callback?: (error: Error | null, result?: string) => void, ): Promise { const parsedProxyUrl = new URL(proxyUrl); if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) { @@ -94,13 +92,12 @@ export async function createTunnel( }); }); - return nodeify(promise, callback); + return promise; } export async function closeTunnel( serverPath: string, - closeConnections: boolean | undefined, - callback: (error: Error | null, result?: boolean) => void, + closeConnections?: boolean, ): Promise { const { hostname, port } = new URL(`tcp://${serverPath}`); if (!hostname) throw new Error('serverPath must contain hostname'); @@ -131,5 +128,5 @@ export async function closeTunnel( }); })); - return nodeify(promise, callback); + return promise; } diff --git a/src/utils/nodeify.ts b/src/utils/nodeify.ts deleted file mode 100644 index 378124b8..00000000 --- a/src/utils/nodeify.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Replacement for Bluebird's Promise.nodeify() -export const nodeify = async (promise: Promise, callback?: (error: Error | null, result?: T) => void): Promise => { - if (typeof callback !== 'function') return promise; - - promise.then( - (result) => callback(null, result), - callback, - ).catch((error) => { - // Need to .catch because it doesn't crash the process on Node.js 14 - process.nextTick(() => { - throw error; - }); - }); - - return promise; -}; diff --git a/test/anonymize_proxy.js b/test/anonymize_proxy.js index e0c1b87f..b18f3b13 100644 --- a/test/anonymize_proxy.js +++ b/test/anonymize_proxy.js @@ -137,129 +137,78 @@ describe('utils.anonymizeProxy', function () { expectThrowsAsync(async () => { await anonymizeProxy({ url: 'https://whatever.com' }); }); }); - it('keeps already anonymous proxies (both with callbacks and promises)', () => { - return Promise.resolve() - .then(() => { - return anonymizeProxy('http://whatever:4567'); - }) - .then((anonymousProxyUrl) => { - expect(anonymousProxyUrl).to.eql('http://whatever:4567'); - }) - .then(() => { - return new Promise((resolve, reject) => { - anonymizeProxy('http://whatever:4567', (err, result) => { - if (err) return reject(err); - resolve(result); - }); - }); - }) - .then((anonymousProxyUrl) => { - expect(anonymousProxyUrl).to.eql('http://whatever:4567'); - }); - }); + it('keeps already anonymous proxies', async () => { + const anonymousProxyUrl = await anonymizeProxy('http://whatever:4567'); + expect(anonymousProxyUrl).to.eql('http://whatever:4567'); - it('anonymizes authenticated upstream proxy (both with callbacks and promises)', () => { - let proxyUrl1; - let proxyUrl2; - return Promise.resolve() - .then(() => { - return Promise.all([ - anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`), - new Promise((resolve, reject) => { - anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`, (err, result) => { - if (err) return reject(err); - resolve(result); - }); - }), - ]); - }) - .then((results) => { - [proxyUrl1, proxyUrl2] = results; - expect(proxyUrl1).to.not.contain(`${proxyPort}`); - expect(proxyUrl2).to.not.contain(`${proxyPort}`); - expect(proxyUrl1).to.not.equal(proxyUrl2); + const anonymousProxyUrl2 = await anonymizeProxy('http://whatever:4567'); + expect(anonymousProxyUrl2).to.eql('http://whatever:4567'); + }); - // Test call through proxy 1 - wasProxyCalled = false; - return requestPromised({ - uri: `http://localhost:${testServerPort}`, - proxy: proxyUrl1, - expectBodyContainsText: 'Hello World!', - }); - }) - .then(() => { - expect(wasProxyCalled).to.equal(true); - }) - .then(() => { - // Test call through proxy 2 - wasProxyCalled = false; - return requestPromised({ - uri: `http://localhost:${testServerPort}`, - proxy: proxyUrl2, - expectBodyContainsText: 'Hello World!', - }); - }) - .then(() => { - expect(wasProxyCalled).to.equal(true); - }) - .then(() => { - // Test again call through proxy 1 - wasProxyCalled = false; - return requestPromised({ - uri: `http://localhost:${testServerPort}`, - proxy: proxyUrl1, - expectBodyContainsText: 'Hello World!', - }); - }) - .then(() => { - expect(wasProxyCalled).to.equal(true); - }) - .then(() => closeAnonymizedProxy(proxyUrl1, true)) - .then((closed) => { - expect(closed).to.eql(true); + it('anonymizes authenticated upstream proxy', async () => { + const [proxyUrl1, proxyUrl2] = await Promise.all([ + anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`), + anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`), + ]); + + expect(proxyUrl1).to.not.contain(`${proxyPort}`); + expect(proxyUrl2).to.not.contain(`${proxyPort}`); + expect(proxyUrl1).to.not.equal(proxyUrl2); + + // Test call through proxy 1 + wasProxyCalled = false; + await requestPromised({ + uri: `http://localhost:${testServerPort}`, + proxy: proxyUrl1, + expectBodyContainsText: 'Hello World!', + }); + expect(wasProxyCalled).to.equal(true); + + // Test call through proxy 2 + wasProxyCalled = false; + await requestPromised({ + uri: `http://localhost:${testServerPort}`, + proxy: proxyUrl2, + expectBodyContainsText: 'Hello World!', + }); + expect(wasProxyCalled).to.equal(true); + + // Test again call through proxy 1 + wasProxyCalled = false; + await requestPromised({ + uri: `http://localhost:${testServerPort}`, + proxy: proxyUrl1, + expectBodyContainsText: 'Hello World!', + }); + expect(wasProxyCalled).to.equal(true); - // Test proxy is really closed - return requestPromised({ - uri: proxyUrl1, - }) - .then(() => { - assert.fail(); - }) - .catch((err) => { - // Node.js 20+ may return 'socket hang up' instead of 'ECONNREFUSED' - const validErrors = ['ECONNREFUSED', 'socket hang up']; - expect(validErrors.some((e) => err.message.includes(e))).to.equal(true); - }); - }) - .then(() => { - // Test callback-style - return new Promise((resolve, reject) => { - closeAnonymizedProxy(proxyUrl2, true, (err, closed) => { - if (err) return reject(err); - resolve(closed); - }); - }); - }) - .then((closed) => { - expect(closed).to.eql(true); + // Close proxy 1 and verify + const closed1 = await closeAnonymizedProxy(proxyUrl1, true); + expect(closed1).to.eql(true); - // Test the second-time call to close - return closeAnonymizedProxy(proxyUrl1, true); - }) - .then((closed) => { - expect(closed).to.eql(false); - - // Test callback-style - return new Promise((resolve, reject) => { - closeAnonymizedProxy(proxyUrl2, false, (err, closed2) => { - if (err) return reject(err); - resolve(closed2); - }); - }); - }) - .then((closed) => { - expect(closed).to.eql(false); + // Test proxy is really closed + try { + await requestPromised({ + uri: proxyUrl1, }); + assert.fail(); + } catch (err) { + // Node.js 20+ may return 'socket hang up' instead of 'ECONNREFUSED' + const validErrors = ['ECONNREFUSED', 'socket hang up']; + expect(validErrors.some((e) => err.message.includes(e))).to.equal(true); + } + + // Close proxy 2 + const closed2 = await closeAnonymizedProxy(proxyUrl2, true); + expect(closed2).to.eql(true); + + // Test the second-time call to close (should return false) + const closed1Again = await closeAnonymizedProxy(proxyUrl1, true); + expect(closed1Again).to.eql(false); + + // Test another second-time call to close + const closed2Again = await closeAnonymizedProxy(proxyUrl2, false); + expect(closed2Again).to.eql(false); }); it('handles many concurrent calls without port collision', () => { diff --git a/test/anonymize_proxy_no_password.js b/test/anonymize_proxy_no_password.js index d9d846f0..5a710aae 100644 --- a/test/anonymize_proxy_no_password.js +++ b/test/anonymize_proxy_no_password.js @@ -93,20 +93,14 @@ const requestPromised = (opts) => { describe('utils.anonymizeProxyNoPassword', function () { // Need larger timeout for Travis CI this.timeout(5 * 1000); - it('anonymizes authenticated with no password upstream proxy (both with callbacks and promises)', () => { + it('anonymizes authenticated with no password upstream proxy', () => { let proxyUrl1; let proxyUrl2; return Promise.resolve() .then(() => { return Promise.all([ anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`), - new Promise((resolve, reject) => { - anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`, - (err, result) => { - if (err) return reject(err); - resolve(result); - }); - }), + anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`), ]); }) .then((results) => { @@ -170,34 +164,20 @@ describe('utils.anonymizeProxyNoPassword', function () { expect(validErrors.some((e) => err.message.includes(e))).to.equal(true); }); }) - .then(() => { - // Test callback-style - return new Promise((resolve, reject) => { - closeAnonymizedProxy(proxyUrl2, true, (err, closed) => { - if (err) return reject(err); - resolve(closed); - }); - }); - }) - .then((closed) => { + .then(async () => { + // Test async/await style + const closed = await closeAnonymizedProxy(proxyUrl2, true); expect(closed).to.eql(true); // Test the second-time call to close return closeAnonymizedProxy(proxyUrl1, true); }) - .then((closed) => { + .then(async (closed) => { expect(closed).to.eql(false); - // Test callback-style - return new Promise((resolve, reject) => { - closeAnonymizedProxy(proxyUrl2, false, (err, closed) => { - if (err) return reject(err); - resolve(closed); - }); - }); - }) - .then((closed) => { - expect(closed).to.eql(false); + // Test async/await style + const closed2 = await closeAnonymizedProxy(proxyUrl2, false); + expect(closed2).to.eql(false); }); }); }); diff --git a/test/ee-memory-leak.js b/test/ee-memory-leak.js index d0b71cb8..b5ca6289 100644 --- a/test/ee-memory-leak.js +++ b/test/ee-memory-leak.js @@ -23,7 +23,7 @@ describe('ProxyChain server', () => { server.close(); }); - it('does not leak events', (done) => { + it('does not leak events', async () => { let socket; let registeredCount; proxyServer.server.prependOnceListener('request', (request) => { @@ -31,31 +31,29 @@ describe('ProxyChain server', () => { registeredCount = socket.listenerCount('error'); }); - const callback = () => { - assert.equal(socket.listenerCount('error'), registeredCount); - done(); - }; + await proxyServer.listen(); + const proxyServerPort = proxyServer.server.address().port; - proxyServer.listen(async () => { - const proxyServerPort = proxyServer.server.address().port; + const requestCount = 20; - const requestCount = 20; - - const client = net.connect({ - host: 'localhost', - port: proxyServerPort, - }); + const client = net.connect({ + host: 'localhost', + port: proxyServerPort, + }); - client.setTimeout(100); + client.setTimeout(100); + await new Promise((resolve) => { client.on('timeout', () => { client.destroy(); - callback(); + resolve(); }); for (let i = 0; i < requestCount; i++) { client.write(`GET http://localhost:${port} HTTP/1.1\r\nhost: localhost:${port}\r\nconnection: keep-alive\r\n\r\n`); } }); + + assert.equal(socket.listenerCount('error'), registeredCount); }); }); diff --git a/test/server.js b/test/server.js index 310f8466..cd8acd09 100644 --- a/test/server.js +++ b/test/server.js @@ -1354,50 +1354,50 @@ describe('non-200 upstream connect response', () => { } }); - it('fails downstream with 590', (done) => { + it('fails downstream with 590', async () => { const server = http.createServer(); server.on('connect', (_request, socket) => { socket.once('error', () => {}); socket.end('HTTP/1.1 403 Forbidden\r\ncontent-length: 1\r\n\r\na'); }); - server.listen(() => { - const serverPort = server.address().port; - const proxyServer = new ProxyChain.Server({ - port: 0, - prepareRequestFunction: () => { - return { - upstreamProxyUrl: `http://localhost:${serverPort}`, - }; + await new Promise((resolve) => server.listen(resolve)); + const serverPort = server.address().port; + const proxyServer = new ProxyChain.Server({ + port: 0, + prepareRequestFunction: () => { + return { + upstreamProxyUrl: `http://localhost:${serverPort}`, + }; + }, + }); + await proxyServer.listen(); + const proxyServerPort = proxyServer.port; + + await new Promise((resolve) => { + const req = http.request({ + method: 'CONNECT', + host: 'localhost', + port: proxyServerPort, + path: 'example.com:443', + headers: { + host: 'example.com:443', }, }); - proxyServer.listen(() => { - const proxyServerPort = proxyServer.port; - - const req = http.request({ - method: 'CONNECT', - host: 'localhost', - port: proxyServerPort, - path: 'example.com:443', - headers: { - host: 'example.com:443', - }, - }); - req.once('connect', (response, socket, head) => { - expect(response.statusCode).to.equal(590); - expect(response.statusMessage).to.equal('UPSTREAM403'); - expect(head.length).to.equal(0); - success = true; + req.once('connect', (response, socket, head) => { + expect(response.statusCode).to.equal(590); + expect(response.statusMessage).to.equal('UPSTREAM403'); + expect(head.length).to.equal(0); + success = true; - socket.once('close', () => { - proxyServer.close(); - server.close(); + socket.once('close', () => { + proxyServer.close(); + server.close(); - done(); - }); + resolve(); }); - - req.end(); }); + + req.end(); }); }); }); diff --git a/test/socks.js b/test/socks.js index dcbd6479..1c7f18bf 100644 --- a/test/socks.js +++ b/test/socks.js @@ -15,64 +15,50 @@ describe('SOCKS protocol', () => { if (anonymizeProxyUrl) ProxyChain.closeAnonymizedProxy(anonymizeProxyUrl, true); }); - it('works without auth', (done) => { - portastic.find({ min: 50000, max: 50250 }).then((ports) => { - const [socksPort, proxyPort] = ports; - socksServer = socksv5.createServer((info, accept) => { - accept(); - }); - socksServer.listen(socksPort, '0.0.0.0', () => { - socksServer.useAuth(socksv5.auth.None()); + it('works without auth', async () => { + const ports = await portastic.find({ min: 50000, max: 50250 }); + const [socksPort, proxyPort] = ports; + socksServer = socksv5.createServer((info, accept) => { + accept(); + }); + await new Promise((resolve) => socksServer.listen(socksPort, '0.0.0.0', resolve)); + socksServer.useAuth(socksv5.auth.None()); - proxyServer = new ProxyChain.Server({ - port: proxyPort, - prepareRequestFunction() { - return { - upstreamProxyUrl: `socks://127.0.0.1:${socksPort}`, - }; - }, - }); - proxyServer.listen(() => { - gotScraping.get({ url: 'https://example.com', proxyUrl: `http://127.0.0.1:${proxyPort}` }) - .then((response) => { - expect(response.body).to.contain('Example Domain'); - done(); - }) - .catch(done); - }); - }); + proxyServer = new ProxyChain.Server({ + port: proxyPort, + prepareRequestFunction() { + return { + upstreamProxyUrl: `socks://127.0.0.1:${socksPort}`, + }; + }, }); + await proxyServer.listen(); + const response = await gotScraping.get({ url: 'https://example.com', proxyUrl: `http://127.0.0.1:${proxyPort}` }); + expect(response.body).to.contain('Example Domain'); }).timeout(10 * 1000); - it('work with auth', (done) => { - portastic.find({ min: 50250, max: 50500 }).then((ports) => { - const [socksPort, proxyPort] = ports; - socksServer = socksv5.createServer((info, accept) => { - accept(); - }); - socksServer.listen(socksPort, '0.0.0.0', () => { - socksServer.useAuth(socksv5.auth.UserPassword((user, password, cb) => { - cb(user === 'proxy-ch@in' && password === 'rules!'); - })); + it('work with auth', async () => { + const ports = await portastic.find({ min: 50250, max: 50500 }); + const [socksPort, proxyPort] = ports; + socksServer = socksv5.createServer((info, accept) => { + accept(); + }); + await new Promise((resolve) => socksServer.listen(socksPort, '0.0.0.0', resolve)); + socksServer.useAuth(socksv5.auth.UserPassword((user, password, cb) => { + cb(user === 'proxy-ch@in' && password === 'rules!'); + })); - proxyServer = new ProxyChain.Server({ - port: proxyPort, - prepareRequestFunction() { - return { - upstreamProxyUrl: `socks://proxy-ch@in:rules!@127.0.0.1:${socksPort}`, - }; - }, - }); - proxyServer.listen(() => { - gotScraping.get({ url: 'https://example.com', proxyUrl: `http://127.0.0.1:${proxyPort}` }) - .then((response) => { - expect(response.body).to.contain('Example Domain'); - done(); - }) - .catch(done); - }); - }); + proxyServer = new ProxyChain.Server({ + port: proxyPort, + prepareRequestFunction() { + return { + upstreamProxyUrl: `socks://proxy-ch@in:rules!@127.0.0.1:${socksPort}`, + }; + }, }); + await proxyServer.listen(); + const response = await gotScraping.get({ url: 'https://example.com', proxyUrl: `http://127.0.0.1:${proxyPort}` }); + expect(response.body).to.contain('Example Domain'); }).timeout(10 * 1000); it('works with anonymizeProxy', (done) => { diff --git a/test/tcp_tunnel.js b/test/tcp_tunnel.js index 8b83c0da..74734b83 100644 --- a/test/tcp_tunnel.js +++ b/test/tcp_tunnel.js @@ -78,7 +78,7 @@ describe('tcp_tunnel.createTunnel', () => { .finally(() => closeServer(proxyServer, proxyServerConnections)) .finally(() => closeServer(targetService, targetServiceConnections)); }); - it('correctly tunnels to tcp service and then is able to close the connection when used with callbacks', () => { + it('correctly tunnels to tcp service and then is able to close the connection (async/await)', async () => { const proxyServerConnections = []; const proxyServer = proxy(http.createServer()); @@ -93,19 +93,16 @@ describe('tcp_tunnel.createTunnel', () => { conn.on('error', (err) => { throw err; }); }); - return serverListen(proxyServer, 0) - .then(() => serverListen(targetService, 0)) - .then((targetServicePort) => new Promise((resolve, reject) => { - createTunnel(`http://localhost:${proxyServer.address().port}`, `localhost:${targetServicePort}`, {}, (err, tunnel) => { - if (err) return reject(err); - return resolve(tunnel); - }); - }).then((tunnel) => closeTunnel(tunnel, true)) - .then((result) => { - assert.equal(result, true); - })) - .finally(() => closeServer(proxyServer, proxyServerConnections)) - .finally(() => closeServer(targetService, targetServiceConnections)); + try { + await serverListen(proxyServer, 0); + const targetServicePort = await serverListen(targetService, 0); + const tunnel = await createTunnel(`http://localhost:${proxyServer.address().port}`, `localhost:${targetServicePort}`, {}); + const result = await closeTunnel(tunnel, true); + assert.equal(result, true); + } finally { + await closeServer(proxyServer, proxyServerConnections); + await closeServer(targetService, targetServiceConnections); + } }); it('creates tunnel that is able to transfer data', () => { let tunnel; diff --git a/test/tools.js b/test/tools.js index dc0021da..c727d1ad 100644 --- a/test/tools.js +++ b/test/tools.js @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { redactUrl } from '../dist/utils/redact_url.js'; import { isHopByHopHeader } from '../dist/utils/is_hop_by_hop_header.js'; import { parseAuthorizationHeader } from '../dist/utils/parse_authorization_header.js'; -import { nodeify } from '../dist/utils/nodeify.js'; describe('tools.redactUrl()', () => { it('works', () => { @@ -135,53 +134,3 @@ describe('tools.parseAuthorizationHeader()', () => { }); }); }); - -const asyncFunction = async (throwError) => { - if (throwError) throw new Error('Test error'); - return 123; -}; - -describe('tools.nodeify()', () => { - it('works', async () => { - { - // Test promised result - const promise = asyncFunction(false); - const result = await nodeify(promise, null); - expect(result).to.eql(123); - } - - { - // Test promised exception - const promise = asyncFunction(true); - try { - await nodeify(promise, null); - throw new Error('This should not be reached!'); - } catch (e) { - expect(e.message).to.eql('Test error'); - } - } - - { - // Test callback result - const promise = asyncFunction(false); - await new Promise((resolve) => { - nodeify(promise, (error, result) => { - expect(result).to.eql(123); - resolve(); - }); - }); - } - - { - // Test callback error - const promise = asyncFunction(true); - await new Promise((resolve) => { - nodeify(promise, (error, result) => { - expect(result, undefined); - expect(error.message).to.eql('Test error'); - resolve(); - }); - }); - } - }); -}); From e888ac66850a780f9fe6fb4fa3f9eec258283165 Mon Sep 17 00:00:00 2001 From: Yurii Bliuchak <1957659+bliuchak@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:49:45 +0100 Subject: [PATCH 2/2] fix: resolve flaky https test --- test/https-server.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/https-server.js b/test/https-server.js index 75e69bd9..ab9eb57a 100644 --- a/test/https-server.js +++ b/test/https-server.js @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import net from 'node:net'; import path from 'node:path'; import tls from 'node:tls'; import { fileURLToPath } from 'node:url'; @@ -20,8 +21,16 @@ it('handles TLS handshake failures gracefully and continues accepting connection let server; let badSocket; let goodSocket; + let targetServer; try { + // Create a local TCP server as the CONNECT target to avoid external network dependency. + targetServer = net.createServer((socket) => { + socket.on('error', () => {}); + }); + await new Promise((resolve) => targetServer.listen(0, '127.0.0.1', resolve)); + const targetPort = targetServer.address().port; + server = new Server({ port: 0, serverType: 'https', @@ -105,8 +114,8 @@ it('handles TLS handshake failures gracefully and continues accepting connection expect(goodSocketConnected).to.equal(true, 'Good socket should have connected'); - // Write the CONNECT request. - goodSocket.write('CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n'); + // Write the CONNECT request to local target server. + goodSocket.write(`CONNECT 127.0.0.1:${targetPort} HTTP/1.1\r\nHost: 127.0.0.1:${targetPort}\r\n\r\n`); const response = await new Promise((resolve, reject) => { const goodSocketTimeout = setTimeout(() => { @@ -149,6 +158,9 @@ it('handles TLS handshake failures gracefully and continues accepting connection if (server) { await server.close(true); } + if (targetServer) { + await new Promise((resolve) => targetServer.close(resolve)); + } } });