Skip to content

Commit 7ca6bb3

Browse files
authored
fix(proxy agent): respect connectTimeout (#5011)
* fix(proxy agent): respect connectTimeout Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com> * fix Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com> --------- Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com>
1 parent 30e9f98 commit 7ca6bb3

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

lib/dispatcher/proxy-agent.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class ProxyAgent extends DispatcherBase {
104104
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
105105
}
106106

107-
const { proxyTunnel = true } = opts
107+
const { proxyTunnel = true, connectTimeout } = opts
108108

109109
super()
110110

@@ -128,9 +128,9 @@ class ProxyAgent extends DispatcherBase {
128128
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
129129
}
130130

131-
const connect = buildConnector({ ...opts.proxyTls })
132-
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
133-
this[kConnectEndpointHTTP1] = buildConnector({ ...opts.requestTls, allowH2: false })
131+
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls })
132+
this[kConnectEndpoint] = buildConnector({ timeout: connectTimeout, ...opts.requestTls })
133+
this[kConnectEndpointHTTP1] = buildConnector({ timeout: connectTimeout, ...opts.requestTls, allowH2: false })
134134

135135
const agentFactory = opts.factory || defaultAgentFactory
136136
const factory = (origin, options) => {

test/proxy-agent.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ const { tspl } = require('@matteo.collina/tspl')
44
const { test, after } = require('node:test')
55
const diagnosticsChannel = require('node:diagnostics_channel')
66
const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..')
7-
const { InvalidArgumentError, SecureProxyConnectionError } = require('../lib/core/errors')
7+
const { InvalidArgumentError, ConnectTimeoutError, SecureProxyConnectionError } = require('../lib/core/errors')
88
const ProxyAgent = require('../lib/dispatcher/proxy-agent')
99
const Pool = require('../lib/dispatcher/pool')
1010
const { createServer } = require('node:http')
1111
const https = require('node:https')
12+
const net = require('node:net')
1213
const { Socket } = require('node:net')
1314
const { createProxy } = require('proxy')
1415

@@ -121,6 +122,65 @@ test('should accept string, URL and object as options', (t) => {
121122
t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' }))
122123
})
123124

125+
test('ProxyAgent forwards connectTimeout to the proxy connector', async (t) => {
126+
t = tspl(t, { plan: 4 })
127+
128+
const originalConnect = net.connect
129+
let connect
130+
let socket
131+
const proxyAgent = new ProxyAgent({
132+
uri: 'http://localhost:9000',
133+
connectTimeout: 1e3,
134+
clientFactory (_origin, options) {
135+
connect = options.connect
136+
return {
137+
close () {
138+
return Promise.resolve()
139+
},
140+
destroy () {
141+
return Promise.resolve()
142+
}
143+
}
144+
}
145+
})
146+
147+
try {
148+
net.connect = function (options) {
149+
return new net.Socket(options)
150+
}
151+
152+
t.ok(typeof connect === 'function')
153+
154+
const timeout = setTimeout(() => {
155+
if (socket && !socket.destroyed) {
156+
socket.destroy()
157+
}
158+
t.fail('connectTimeout was not forwarded to the proxy connector')
159+
}, 2e3)
160+
161+
await new Promise((resolve, reject) => {
162+
socket = connect({ hostname: 'localhost', protocol: 'http:', port: 9000 }, (err) => {
163+
try {
164+
t.ok(err instanceof ConnectTimeoutError)
165+
t.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
166+
t.strictEqual(err.message, 'Connect Timeout Error (attempted address: localhost:9000, timeout: 1000ms)')
167+
clearTimeout(timeout)
168+
resolve()
169+
} catch (error) {
170+
clearTimeout(timeout)
171+
reject(error)
172+
}
173+
})
174+
})
175+
} finally {
176+
net.connect = originalConnect
177+
if (socket && !socket.destroyed) {
178+
socket.destroy()
179+
}
180+
await proxyAgent.close()
181+
}
182+
})
183+
124184
test('use proxy-agent to connect through proxy (keep alive)', async (t) => {
125185
t = tspl(t, { plan: 10 })
126186
const server = await buildServer()

0 commit comments

Comments
 (0)