Skip to content

Commit f05e00a

Browse files
rossilor95crysmags
authored andcommitted
feat: refactor ProxyAgent constructor to also accept single URL argument (nodejs#2810)
* feat: add support for opts as URL in ProxyAgent * test: update ProxyAgent unit tests * docs: update ProxyAgent documentation
1 parent 5edd986 commit f05e00a

File tree

3 files changed

+73
-35
lines changed

3 files changed

+73
-35
lines changed

docs/docs/api/ProxyAgent.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Returns: `ProxyAgent`
1616

1717
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
1818

19-
* **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string.
19+
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
2020
* **token** `string` (optional) - It can be passed by a string of token for authentication.
2121
* **auth** `string` (**deprecated**) - Use token.
2222
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
@@ -30,6 +30,8 @@ import { ProxyAgent } from 'undici'
3030

3131
const proxyAgent = new ProxyAgent('my.proxy.server')
3232
// or
33+
const proxyAgent = new ProxyAgent(new URL('my.proxy.server'))
34+
// or
3335
const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' })
3436
```
3537

lib/proxy-agent.js

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,35 @@ function defaultProtocolPort (protocol) {
1919
return protocol === 'https:' ? 443 : 80
2020
}
2121

22-
function buildProxyOptions (opts) {
23-
if (typeof opts === 'string') {
24-
opts = { uri: opts }
25-
}
26-
27-
if (!opts || !opts.uri) {
28-
throw new InvalidArgumentError('Proxy opts.uri is mandatory')
29-
}
30-
31-
return {
32-
uri: opts.uri,
33-
protocol: opts.protocol || 'https'
34-
}
35-
}
36-
3722
function defaultFactory (origin, opts) {
3823
return new Pool(origin, opts)
3924
}
4025

4126
class ProxyAgent extends DispatcherBase {
4227
constructor (opts) {
43-
super(opts)
44-
this[kProxy] = buildProxyOptions(opts)
45-
this[kAgent] = new Agent(opts)
46-
this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
47-
? opts.interceptors.ProxyAgent
48-
: []
28+
super()
4929

50-
if (typeof opts === 'string') {
51-
opts = { uri: opts }
52-
}
53-
54-
if (!opts || !opts.uri) {
55-
throw new InvalidArgumentError('Proxy opts.uri is mandatory')
30+
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
31+
throw new InvalidArgumentError('Proxy uri is mandatory')
5632
}
5733

5834
const { clientFactory = defaultFactory } = opts
59-
6035
if (typeof clientFactory !== 'function') {
6136
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
6237
}
6338

39+
const url = this.#getUrl(opts)
40+
const { href, origin, port, protocol, username, password } = url
41+
42+
this[kProxy] = { uri: href, protocol }
43+
this[kAgent] = new Agent(opts)
44+
this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
45+
? opts.interceptors.ProxyAgent
46+
: []
6447
this[kRequestTls] = opts.requestTls
6548
this[kProxyTls] = opts.proxyTls
6649
this[kProxyHeaders] = opts.headers || {}
6750

68-
const resolvedUrl = new URL(opts.uri)
69-
const { origin, port, username, password } = resolvedUrl
70-
7151
if (opts.auth && opts.token) {
7252
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
7353
} else if (opts.auth) {
@@ -81,7 +61,7 @@ class ProxyAgent extends DispatcherBase {
8161

8262
const connect = buildConnector({ ...opts.proxyTls })
8363
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
84-
this[kClient] = clientFactory(resolvedUrl, { connect })
64+
this[kClient] = clientFactory(url, { connect })
8565
this[kAgent] = new Agent({
8666
...opts,
8767
connect: async (opts, callback) => {
@@ -138,6 +118,20 @@ class ProxyAgent extends DispatcherBase {
138118
)
139119
}
140120

121+
/**
122+
* @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts
123+
* @returns {URL}
124+
*/
125+
#getUrl (opts) {
126+
if (typeof opts === 'string') {
127+
return new URL(opts)
128+
} else if (opts instanceof URL) {
129+
return opts
130+
} else {
131+
return new URL(opts.uri)
132+
}
133+
}
134+
141135
async [kClose] () {
142136
await this[kAgent].close()
143137
await this[kClient].close()

test/proxy-agent.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ test('using auth in combination with token should throw', (t) => {
2929
)
3030
})
3131

32-
test('should accept string and object as options', (t) => {
33-
t = tspl(t, { plan: 2 })
32+
test('should accept string, URL and object as options', (t) => {
33+
t = tspl(t, { plan: 3 })
3434
t.doesNotThrow(() => new ProxyAgent('http://example.com'))
35+
t.doesNotThrow(() => new ProxyAgent(new URL('http://example.com')))
3536
t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' }))
3637
})
3738

@@ -148,6 +149,47 @@ test('use proxy-agent to connect through proxy using path with params', async (t
148149
proxyAgent.close()
149150
})
150151

152+
test('use proxy-agent to connect through proxy with basic auth in URL', async (t) => {
153+
t = tspl(t, { plan: 7 })
154+
const server = await buildServer()
155+
const proxy = await buildProxy()
156+
157+
const serverUrl = `http://localhost:${server.address().port}`
158+
const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`)
159+
const proxyAgent = new ProxyAgent(proxyUrl)
160+
const parsedOrigin = new URL(serverUrl)
161+
162+
proxy.authenticate = function (req, fn) {
163+
t.ok(true, 'authentication should be called')
164+
fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
165+
}
166+
proxy.on('connect', () => {
167+
t.ok(true, 'proxy should be called')
168+
})
169+
170+
server.on('request', (req, res) => {
171+
t.strictEqual(req.url, '/hello?foo=bar')
172+
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
173+
res.setHeader('content-type', 'application/json')
174+
res.end(JSON.stringify({ hello: 'world' }))
175+
})
176+
177+
const {
178+
statusCode,
179+
headers,
180+
body
181+
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
182+
const json = await body.json()
183+
184+
t.strictEqual(statusCode, 200)
185+
t.deepStrictEqual(json, { hello: 'world' })
186+
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
187+
188+
server.close()
189+
proxy.close()
190+
proxyAgent.close()
191+
})
192+
151193
test('use proxy-agent with auth', async (t) => {
152194
t = tspl(t, { plan: 7 })
153195
const server = await buildServer()

0 commit comments

Comments
 (0)