From 5f346b562f62435031c176abf8393707b424db7d Mon Sep 17 00:00:00 2001 From: Jamie King Date: Sun, 24 Mar 2024 22:41:19 -0700 Subject: [PATCH 01/10] feat: added EnvHttpProxyAgent Closes #1650 --- index-fetch.js | 4 + index.js | 2 + lib/core/symbols.js | 5 +- lib/dispatcher/dispatcher-base.js | 4 +- lib/dispatcher/env-http-proxy-agent.js | 108 ++++++++ lib/global.js | 4 +- package.json | 1 + test/env-http-proxy-agent.js | 337 +++++++++++++++++++++++++ test/node-fetch/main.js | 4 +- test/node-test/debug.js | 2 +- 10 files changed, 462 insertions(+), 9 deletions(-) create mode 100644 lib/dispatcher/env-http-proxy-agent.js create mode 100644 test/env-http-proxy-agent.js diff --git a/index-fetch.js b/index-fetch.js index dc6c0d05e3e..2db4484756c 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -1,5 +1,9 @@ 'use strict' +const Dispatcher = require('./lib/dispatcher/dispatcher') +const api = require('./lib/api') +Object.assign(Dispatcher.prototype, api) + const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = function fetch (resource, init = undefined) { diff --git a/index.js b/index.js index dcf0be90798..e9fe28e5b43 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const Pool = require('./lib/dispatcher/pool') const BalancedPool = require('./lib/dispatcher/balanced-pool') const Agent = require('./lib/dispatcher/agent') const ProxyAgent = require('./lib/dispatcher/proxy-agent') +const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') const RetryAgent = require('./lib/dispatcher/retry-agent') const errors = require('./lib/core/errors') const util = require('./lib/core/util') @@ -30,6 +31,7 @@ module.exports.Pool = Pool module.exports.BalancedPool = BalancedPool module.exports.Agent = Agent module.exports.ProxyAgent = ProxyAgent +module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent module.exports.RetryAgent = RetryAgent module.exports.RetryHandler = RetryHandler diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 02fda9e251d..879ba833de6 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -60,5 +60,8 @@ module.exports = { kConstruct: Symbol('constructable'), kListeners: Symbol('listeners'), kHTTPContext: Symbol('http context'), - kMaxConcurrentStreams: Symbol('max concurrent streams') + kMaxConcurrentStreams: Symbol('max concurrent streams'), + kNoProxyAgent: Symbol('no proxy agent'), + kHttpProxyAgent: Symbol('http proxy agent'), + kHttpsProxyAgent: Symbol('https proxy agent') } diff --git a/lib/dispatcher/dispatcher-base.js b/lib/dispatcher/dispatcher-base.js index 88e1ca48db7..bd860acdcf4 100644 --- a/lib/dispatcher/dispatcher-base.js +++ b/lib/dispatcher/dispatcher-base.js @@ -6,10 +6,8 @@ const { ClientClosedError, InvalidArgumentError } = require('../core/errors') -const { kDestroy, kClose, kDispatch, kInterceptors } = require('../core/symbols') +const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = require('../core/symbols') -const kDestroyed = Symbol('destroyed') -const kClosed = Symbol('closed') const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') const kInterceptedDispatch = Symbol('Intercepted Dispatch') diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js new file mode 100644 index 00000000000..f2b994b79cf --- /dev/null +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -0,0 +1,108 @@ +'use strict' + +const DispatcherBase = require('./dispatcher-base') +const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols') +const ProxyAgent = require('./proxy-agent') +const Agent = require('./agent') + +const DEFAULT_PORTS = { + 'http:': 80, + 'https:': 443 +} + +class EnvHttpProxyAgent extends DispatcherBase { + constructor (opts) { + super() + + this[kNoProxyAgent] = new Agent(opts) + + const HTTP_PROXY = process.env.HTTP_PROXY || process.env.http_proxy + if (HTTP_PROXY) { + this[kHttpProxyAgent] = new ProxyAgent({ ...opts, uri: HTTP_PROXY }) + } else { + this[kHttpProxyAgent] = this[kNoProxyAgent] + } + + const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.https_proxy + if (HTTPS_PROXY) { + this[kHttpsProxyAgent] = new ProxyAgent({ ...opts, uri: HTTPS_PROXY }) + } else { + this[kHttpsProxyAgent] = this[kHttpProxyAgent] + } + } + + [kDispatch] (opts, handler) { + const url = new URL(opts.origin) + const agent = this.#getProxyAgentForUrl(url) + return agent.dispatch(opts, handler) + } + + async [kClose] () { + await this[kNoProxyAgent].close() + if (!this[kHttpProxyAgent][kClosed]) { + await this[kHttpProxyAgent].close() + } + if (!this[kHttpsProxyAgent][kClosed]) { + await this[kHttpsProxyAgent].close() + } + } + + async [kDestroy] (err) { + await this[kNoProxyAgent].destroy(err) + if (!this[kHttpProxyAgent][kDestroyed]) { + await this[kHttpProxyAgent].destroy(err) + } + if (!this[kHttpsProxyAgent][kDestroyed]) { + await this[kHttpsProxyAgent].destroy(err) + } + } + + #getProxyAgentForUrl (url) { + let { protocol, host: hostname, port } = url + + // Stripping ports in this way instead of using parsedUrl.hostname to make + // sure that the brackets around IPv6 addresses are kept. + hostname = hostname.replace(/:\d*$/, '').toLowerCase() + port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 + if (!this.#shouldProxy(hostname, port)) { + return this[kNoProxyAgent] + } + if (protocol === 'https:') { + return this[kHttpsProxyAgent] + } + return this[kHttpProxyAgent] + } + + #shouldProxy (hostname, port) { + const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy + if (!NO_PROXY) { + return true // Always proxy if NO_PROXY is not set. + } + if (NO_PROXY === '*') { + return false // Never proxy if wildcard is set. + } + + return NO_PROXY.split(/[,\s]/).filter((entry) => !!entry.length).every(function (entry) { + const parsed = entry.match(/^(.+):(\d+)$/) + let parsedHostname = (parsed ? parsed[1] : entry).toLowerCase() + const parsedPort = parsed ? Number.parseInt(parsed[2], 10) : 0 + if (parsedPort && parsedPort !== port) { + return true // Skip if ports don't match. + } + + if (!/^[.*]/.test(parsedHostname)) { + // No wildcards, so proxy if there is not an exact match. + return hostname !== parsedHostname + } + + if (parsedHostname.startsWith('*')) { + // Remove leading wildcard. + parsedHostname = parsedHostname.slice(1) + } + // Don't proxy if the hostname ends with the no_proxy host. + return !hostname.endsWith(parsedHostname) + }) + } +} + +module.exports = EnvHttpProxyAgent diff --git a/lib/global.js b/lib/global.js index 0c7528fa653..b39afc1a0b3 100644 --- a/lib/global.js +++ b/lib/global.js @@ -4,10 +4,10 @@ // this version number must be increased to avoid conflicts. const globalDispatcher = Symbol.for('undici.globalDispatcher.1') const { InvalidArgumentError } = require('./core/errors') -const Agent = require('./dispatcher/agent') +const EnvHttpProxyAgent = require('./dispatcher/env-http-proxy-agent') if (getGlobalDispatcher() === undefined) { - setGlobalDispatcher(new Agent()) + setGlobalDispatcher(new EnvHttpProxyAgent()) } function setGlobalDispatcher (agent) { diff --git a/package.json b/package.json index a3dc801c4dc..6044c737de7 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "node-forge": "^1.3.1", "pre-commit": "^1.2.2", "proxy": "^2.1.1", + "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", "tsd": "^0.31.0", diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js new file mode 100644 index 00000000000..d4d4765528d --- /dev/null +++ b/test/env-http-proxy-agent.js @@ -0,0 +1,337 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test, describe, after, beforeEach, afterEach } = require('node:test') +const sinon = require('sinon') +const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..') +const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed } = require('../lib/core/symbols') + +const env = { ...process.env } + +beforeEach(() => { + ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy'].forEach((varname) => { + delete process.env[varname] + }) +}) + +after(() => { + process.env = { ...env } +}) + +test('does not create any proxy agents if HTTP_PROXY and HTTPS_PROXY are not set', async (t) => { + t = tspl(t, { plan: 4 }) + const dispatcher = new EnvHttpProxyAgent() + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent]) + t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kNoProxyAgent]) + return dispatcher.close() +}) + +test('creates one proxy agent for both http and https when only HTTP_PROXY is defined', async (t) => { + t = tspl(t, { plan: 4 }) + process.env.HTTP_PROXY = 'http://example.com:8080' + const dispatcher = new EnvHttpProxyAgent() + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) + t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) + return dispatcher.close() +}) + +test('creates separate proxy agent for http and https when HTTP_PROXY and HTTPS_PROXY are set', async (t) => { + t = tspl(t, { plan: 5 }) + process.env.HTTP_PROXY = 'http://example.com:8080' + process.env.HTTPS_PROXY = 'http://example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) + t.notDeepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) + t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + return dispatcher.close() +}) + +test('handles lowercase http_proxy and https_proxy', async (t) => { + t = tspl(t, { plan: 5 }) + process.env.http_proxy = 'http://example.com:8080' + process.env.https_proxy = 'http://example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) + t.notDeepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) + t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + return dispatcher.close() +}) + +test('creates a proxy agent only for https when only HTTPS_PROXY is set', async (t) => { + t = tspl(t, { plan: 4 }) + process.env.HTTPS_PROXY = 'http://example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent]) + t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + return dispatcher.close() +}) + +test('closes all agents', async (t) => { + t = tspl(t, { plan: 3 }) + process.env.HTTP_PROXY = 'http://example.com:8080' + process.env.HTTPS_PROXY = 'http://example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + await dispatcher.close() + t.ok(dispatcher[kNoProxyAgent][kClosed]) + t.ok(dispatcher[kHttpProxyAgent][kClosed]) + t.ok(dispatcher[kHttpsProxyAgent][kClosed]) +}) + +test('destroys all agents', async (t) => { + t = tspl(t, { plan: 3 }) + process.env.HTTP_PROXY = 'http://example.com:8080' + process.env.HTTPS_PROXY = 'http://example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + await dispatcher.destroy() + t.ok(dispatcher[kNoProxyAgent][kDestroyed]) + t.ok(dispatcher[kHttpProxyAgent][kDestroyed]) + t.ok(dispatcher[kHttpsProxyAgent][kDestroyed]) +}) + +const createEnvHttpProxyAgentWithMocks = () => { + const testPaths = ['/', '/example', '/32'] + const mockAgent = new MockAgent() + let mockPool + const factory = (origin) => { + mockPool = mockAgent.get(origin) + testPaths.forEach((path) => mockPool.intercept({ path }).reply(200, 'OK')) + return mockPool + } + process.env.HTTP_PROXY = 'http://localhost:8080' + process.env.HTTPS_PROXY = 'http://localhost:8443' + const dispatcher = new EnvHttpProxyAgent({ factory }) + const agentSymbols = [kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent] + agentSymbols.forEach((agent) => { + sinon.spy(dispatcher[agent], 'dispatch') + }) + const reset = () => agentSymbols.forEach((agent) => { + dispatcher[agent].dispatch.resetHistory() + testPaths.forEach((path) => mockPool.intercept({ path }).reply(200, 'OK')) + }) + const usesProxyAgent = async (agent, url) => { + await fetch(url, { dispatcher }) + const result = agentSymbols.every((agentSymbol) => agent === agentSymbol + ? dispatcher[agentSymbol].dispatch.called + : dispatcher[agentSymbol].dispatch.notCalled) + reset() + return result + } + const doesNotProxy = usesProxyAgent.bind(this, kNoProxyAgent) + return { + dispatcher, + doesNotProxy, + usesProxyAgent + } +} + +test('uses the appropriate proxy for the protocol', async (t) => { + t = tspl(t, { plan: 2 }) + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com/')) + t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://example.com/')) + return dispatcher.close() +}) + +describe('NO_PROXY', () => { + let dispatcher + let doesNotProxy + let usesProxyAgent + + beforeEach(() => { + ({ dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()) + }) + + afterEach(() => dispatcher.close()) + + test('set to *', async (t) => { + t = tspl(t, { plan: 2 }) + process.env.NO_PROXY = '*' + t.ok(await doesNotProxy('https://example.com')) + t.ok(await doesNotProxy('http://example.com')) + }) + + test('set but empty', async (t) => { + t = tspl(t, { plan: 1 }) + process.env.NO_PROXY = '' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + }) + + test('no entries (comma)', async (t) => { + t = tspl(t, { plan: 1 }) + process.env.NO_PROXY = ',' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + }) + + test('no entries (whitespace)', async (t) => { + t = tspl(t, { plan: 1 }) + process.env.NO_PROXY = ' ' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + }) + + test('no entries (multiple whitespace / commas)', async (t) => { + t = tspl(t, { plan: 1 }) + process.env.NO_PROXY = ',\t,,,\n, ,\r' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + }) + + test('single host', async (t) => { + t = tspl(t, { plan: 9 }) + process.env.NO_PROXY = 'example' + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:0')) + t.ok(await doesNotProxy('http://example:1337')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + }) + + test('subdomain', async (t) => { + t = tspl(t, { plan: 8 }) + process.env.NO_PROXY = 'sub.example' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await doesNotProxy('http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://no.sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub-example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.sub')) + }) + + test('host + port', async (t) => { + t = tspl(t, { plan: 12 }) + process.env.NO_PROXY = 'example:80, localhost:3000' + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:0')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await doesNotProxy('http://localhost:3000/')) + t.ok(await doesNotProxy('https://localhost:3000/')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost:3001/')) + t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://localhost:3001/')) + }) + + test('host suffix', async (t) => { + t = tspl(t, { plan: 9 }) + process.env.NO_PROXY = '.example' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await doesNotProxy('http://sub.example')) + t.ok(await doesNotProxy('http://sub.example:80')) + t.ok(await doesNotProxy('http://sub.example:1337')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await doesNotProxy('http://a.b.example')) + }) + + test('host suffix with *.', async (t) => { + t = tspl(t, { plan: 9 }) + process.env.NO_PROXY = '*.example' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await doesNotProxy('http://sub.example')) + t.ok(await doesNotProxy('http://sub.example:80')) + t.ok(await doesNotProxy('http://sub.example:1337')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await doesNotProxy('http://a.b.example')) + }) + + test('substring suffix', async (t) => { + t = tspl(t, { plan: 10 }) + process.env.NO_PROXY = '*example' + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:1337')) + t.ok(await doesNotProxy('http://sub.example')) + t.ok(await doesNotProxy('http://sub.example:80')) + t.ok(await doesNotProxy('http://sub.example:1337')) + t.ok(await doesNotProxy('http://prefexample')) + t.ok(await doesNotProxy('http://a.b.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + }) + + test('arbitrary wildcards are NOT supported', async (t) => { + t = tspl(t, { plan: 6 }) + process.env.NO_PROXY = '.*example' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + }) + + test('IP addresses', async (t) => { + t = tspl(t, { plan: 12 }) + process.env.NO_PROXY = '[::1],[::2]:80,10.0.0.1,10.0.0.2:80' + t.ok(await doesNotProxy('http://[::1]/')) + t.ok(await doesNotProxy('http://[::1]:80/')) + t.ok(await doesNotProxy('http://[::1]:1337/')) + t.ok(await doesNotProxy('http://[::2]/')) + t.ok(await doesNotProxy('http://[::2]:80/')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://[::2]:1337/')) + t.ok(await doesNotProxy('http://10.0.0.1/')) + t.ok(await doesNotProxy('http://10.0.0.1:80/')) + t.ok(await doesNotProxy('http://10.0.0.1:1337/')) + t.ok(await doesNotProxy('http://10.0.0.2/')) + t.ok(await doesNotProxy('http://10.0.0.2:80/')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://10.0.0.2:1337/')) + }) + + test('CIDR is NOT supported', async (t) => { + t = tspl(t, { plan: 2 }) + env.NO_PROXY = '127.0.0.1/32' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1/32')) + }) + + test('127.0.0.1 does NOT match localhost', async (t) => { + t = tspl(t, { plan: 2 }) + process.env.NO_PROXY = '127.0.0.1' + t.ok(await doesNotProxy('http://127.0.0.1')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost')) + }) + + test('protocols that have a default port', async (t) => { + t = tspl(t, { plan: 6 }) + process.env.NO_PROXY = 'xxx:21,xxx:70,xxx:80,xxx:443' + t.ok(await doesNotProxy('http://xxx')) + t.ok(await doesNotProxy('http://xxx:80')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://xxx:1337')) + t.ok(await doesNotProxy('https://xxx')) + t.ok(await doesNotProxy('https://xxx:443')) + t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://xxx:1337')) + }) + + test('should not be case-sensitive', async (t) => { + t = tspl(t, { plan: 6 }) + process.env.no_proxy = 'XXX YYY ZzZ' + t.ok(await doesNotProxy('http://xxx')) + t.ok(await doesNotProxy('http://XXX')) + t.ok(await doesNotProxy('http://yyy')) + t.ok(await doesNotProxy('http://YYY')) + t.ok(await doesNotProxy('http://ZzZ')) + t.ok(await doesNotProxy('http://zZz')) + }) +}) diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index 3ba4a929af0..d9d2d376788 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -17,7 +17,7 @@ const { FormData, Response, setGlobalDispatcher, - Agent + EnvHttpProxyAgent } = require('../../index.js') const HeadersOrig = require('../../lib/web/fetch/headers.js').Headers const ResponseOrig = require('../../lib/web/fetch/response.js').Response @@ -34,7 +34,7 @@ describe('node-fetch', () => { before(async () => { await local.start() - setGlobalDispatcher(new Agent({ + setGlobalDispatcher(new EnvHttpProxyAgent({ connect: { rejectUnauthorized: false } diff --git a/test/node-test/debug.js b/test/node-test/debug.js index 3e6ca0bc0ef..625acb8751b 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -53,7 +53,7 @@ test('debug#fetch', async t => { process.execPath, [join(__dirname, '../fixtures/fetch.js')], { - env: Object.assign({}, process.env, { NODE_DEBUG: 'fetch' }) + env: Object.assign({}, process.env, { NODE_DEBUG: 'fetch', NO_PROXY: '*' }) } ) const chunks = [] From 3b7cd3bdf595f37377a2eea4e336699f1cb2ade3 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Mon, 25 Mar 2024 17:54:50 -0700 Subject: [PATCH 02/10] refactor(env-http-proxy-agent): parse NO_PROXY in constructor --- lib/dispatcher/env-http-proxy-agent.js | 42 +++++++++++++--------- test/env-http-proxy-agent.js | 48 +++++++++++++++++++------- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index f2b994b79cf..7859b4054b4 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -11,6 +11,10 @@ const DEFAULT_PORTS = { } class EnvHttpProxyAgent extends DispatcherBase { + #neverProxy = false + #alwaysProxy = false + #noProxyEntries = [] + constructor (opts) { super() @@ -29,6 +33,19 @@ class EnvHttpProxyAgent extends DispatcherBase { } else { this[kHttpsProxyAgent] = this[kHttpProxyAgent] } + + const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || '' + this.#neverProxy = NO_PROXY === '*' + this.#noProxyEntries = NO_PROXY.split(/[,\s]/) + .filter(Boolean) + .map((entry) => { + const parsed = entry.match(/^(.+):(\d+)$/) + return { + hostname: (parsed ? parsed[1] : entry).toLowerCase(), + port: parsed ? Number.parseInt(parsed[2], 10) : 0 + } + }) + this.#alwaysProxy = this.#noProxyEntries.length === 0 } [kDispatch] (opts, handler) { @@ -74,33 +91,24 @@ class EnvHttpProxyAgent extends DispatcherBase { } #shouldProxy (hostname, port) { - const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy - if (!NO_PROXY) { - return true // Always proxy if NO_PROXY is not set. + if (this.#alwaysProxy) { + return true // Always proxy if NO_PROXY is not set or empty. } - if (NO_PROXY === '*') { + if (this.#neverProxy) { return false // Never proxy if wildcard is set. } - return NO_PROXY.split(/[,\s]/).filter((entry) => !!entry.length).every(function (entry) { - const parsed = entry.match(/^(.+):(\d+)$/) - let parsedHostname = (parsed ? parsed[1] : entry).toLowerCase() - const parsedPort = parsed ? Number.parseInt(parsed[2], 10) : 0 - if (parsedPort && parsedPort !== port) { + return this.#noProxyEntries.every(function (entry) { + if (entry.port && entry.port !== port) { return true // Skip if ports don't match. } - if (!/^[.*]/.test(parsedHostname)) { + if (!/^[.*]/.test(entry.hostname)) { // No wildcards, so proxy if there is not an exact match. - return hostname !== parsedHostname - } - - if (parsedHostname.startsWith('*')) { - // Remove leading wildcard. - parsedHostname = parsedHostname.slice(1) + return hostname !== entry.hostname } // Don't proxy if the hostname ends with the no_proxy host. - return !hostname.endsWith(parsedHostname) + return !hostname.endsWith(entry.hostname.replace(/^\*/, '')) }) } } diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index d4d4765528d..df361e15660 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -1,7 +1,7 @@ 'use strict' const { tspl } = require('@matteo.collina/tspl') -const { test, describe, after, beforeEach, afterEach } = require('node:test') +const { test, describe, after, beforeEach } = require('node:test') const sinon = require('sinon') const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..') const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed } = require('../lib/core/symbols') @@ -143,50 +143,51 @@ test('uses the appropriate proxy for the protocol', async (t) => { }) describe('NO_PROXY', () => { - let dispatcher - let doesNotProxy - let usesProxyAgent - - beforeEach(() => { - ({ dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()) - }) - - afterEach(() => dispatcher.close()) - test('set to *', async (t) => { t = tspl(t, { plan: 2 }) process.env.NO_PROXY = '*' + const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('https://example.com')) t.ok(await doesNotProxy('http://example.com')) + return dispatcher.close() }) test('set but empty', async (t) => { t = tspl(t, { plan: 1 }) process.env.NO_PROXY = '' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + return dispatcher.close() }) test('no entries (comma)', async (t) => { t = tspl(t, { plan: 1 }) process.env.NO_PROXY = ',' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + return dispatcher.close() }) test('no entries (whitespace)', async (t) => { t = tspl(t, { plan: 1 }) process.env.NO_PROXY = ' ' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + return dispatcher.close() }) test('no entries (multiple whitespace / commas)', async (t) => { t = tspl(t, { plan: 1 }) process.env.NO_PROXY = ',\t,,,\n, ,\r' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + return dispatcher.close() }) test('single host', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = 'example' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) @@ -196,11 +197,13 @@ describe('NO_PROXY', () => { t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + return dispatcher.close() }) test('subdomain', async (t) => { t = tspl(t, { plan: 8 }) process.env.NO_PROXY = 'sub.example' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0')) @@ -209,11 +212,13 @@ describe('NO_PROXY', () => { t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://no.sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub-example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.sub')) + return dispatcher.close() }) test('host + port', async (t) => { t = tspl(t, { plan: 12 }) process.env.NO_PROXY = 'example:80, localhost:3000' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) @@ -226,11 +231,13 @@ describe('NO_PROXY', () => { t.ok(await doesNotProxy('https://localhost:3000/')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost:3001/')) t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://localhost:3001/')) + return dispatcher.close() }) test('host suffix', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = '.example' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) @@ -240,11 +247,13 @@ describe('NO_PROXY', () => { t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) t.ok(await doesNotProxy('http://a.b.example')) + return dispatcher.close() }) test('host suffix with *.', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = '*.example' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) @@ -254,11 +263,13 @@ describe('NO_PROXY', () => { t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) t.ok(await doesNotProxy('http://a.b.example')) + return dispatcher.close() }) test('substring suffix', async (t) => { t = tspl(t, { plan: 10 }) process.env.NO_PROXY = '*example' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:1337')) @@ -269,22 +280,26 @@ describe('NO_PROXY', () => { t.ok(await doesNotProxy('http://a.b.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + return dispatcher.close() }) test('arbitrary wildcards are NOT supported', async (t) => { t = tspl(t, { plan: 6 }) process.env.NO_PROXY = '.*example' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + return dispatcher.close() }) test('IP addresses', async (t) => { t = tspl(t, { plan: 12 }) process.env.NO_PROXY = '[::1],[::2]:80,10.0.0.1,10.0.0.2:80' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://[::1]/')) t.ok(await doesNotProxy('http://[::1]:80/')) t.ok(await doesNotProxy('http://[::1]:1337/')) @@ -297,41 +312,50 @@ describe('NO_PROXY', () => { t.ok(await doesNotProxy('http://10.0.0.2/')) t.ok(await doesNotProxy('http://10.0.0.2:80/')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://10.0.0.2:1337/')) + return dispatcher.close() }) test('CIDR is NOT supported', async (t) => { t = tspl(t, { plan: 2 }) - env.NO_PROXY = '127.0.0.1/32' + process.env.NO_PROXY = '127.0.0.1/32' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1/32')) + return dispatcher.close() }) test('127.0.0.1 does NOT match localhost', async (t) => { t = tspl(t, { plan: 2 }) process.env.NO_PROXY = '127.0.0.1' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://127.0.0.1')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost')) + return dispatcher.close() }) test('protocols that have a default port', async (t) => { t = tspl(t, { plan: 6 }) process.env.NO_PROXY = 'xxx:21,xxx:70,xxx:80,xxx:443' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://xxx')) t.ok(await doesNotProxy('http://xxx:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://xxx:1337')) t.ok(await doesNotProxy('https://xxx')) t.ok(await doesNotProxy('https://xxx:443')) t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://xxx:1337')) + return dispatcher.close() }) test('should not be case-sensitive', async (t) => { t = tspl(t, { plan: 6 }) process.env.no_proxy = 'XXX YYY ZzZ' + const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks() t.ok(await doesNotProxy('http://xxx')) t.ok(await doesNotProxy('http://XXX')) t.ok(await doesNotProxy('http://yyy')) t.ok(await doesNotProxy('http://YYY')) t.ok(await doesNotProxy('http://ZzZ')) t.ok(await doesNotProxy('http://zZz')) + return dispatcher.close() }) }) From f31d3f9ca9266a836fa3267b140b00f5ee27463d Mon Sep 17 00:00:00 2001 From: Jamie King Date: Sat, 30 Mar 2024 14:46:39 -0700 Subject: [PATCH 03/10] don't use EnvHttpProxyAgent by default --- index-fetch.js | 4 ---- lib/global.js | 4 ++-- test/node-fetch/main.js | 4 ++-- test/node-test/debug.js | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/index-fetch.js b/index-fetch.js index 2db4484756c..dc6c0d05e3e 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -1,9 +1,5 @@ 'use strict' -const Dispatcher = require('./lib/dispatcher/dispatcher') -const api = require('./lib/api') -Object.assign(Dispatcher.prototype, api) - const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = function fetch (resource, init = undefined) { diff --git a/lib/global.js b/lib/global.js index b39afc1a0b3..0c7528fa653 100644 --- a/lib/global.js +++ b/lib/global.js @@ -4,10 +4,10 @@ // this version number must be increased to avoid conflicts. const globalDispatcher = Symbol.for('undici.globalDispatcher.1') const { InvalidArgumentError } = require('./core/errors') -const EnvHttpProxyAgent = require('./dispatcher/env-http-proxy-agent') +const Agent = require('./dispatcher/agent') if (getGlobalDispatcher() === undefined) { - setGlobalDispatcher(new EnvHttpProxyAgent()) + setGlobalDispatcher(new Agent()) } function setGlobalDispatcher (agent) { diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index d9d2d376788..3ba4a929af0 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -17,7 +17,7 @@ const { FormData, Response, setGlobalDispatcher, - EnvHttpProxyAgent + Agent } = require('../../index.js') const HeadersOrig = require('../../lib/web/fetch/headers.js').Headers const ResponseOrig = require('../../lib/web/fetch/response.js').Response @@ -34,7 +34,7 @@ describe('node-fetch', () => { before(async () => { await local.start() - setGlobalDispatcher(new EnvHttpProxyAgent({ + setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } diff --git a/test/node-test/debug.js b/test/node-test/debug.js index 625acb8751b..3e6ca0bc0ef 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -53,7 +53,7 @@ test('debug#fetch', async t => { process.execPath, [join(__dirname, '../fixtures/fetch.js')], { - env: Object.assign({}, process.env, { NODE_DEBUG: 'fetch', NO_PROXY: '*' }) + env: Object.assign({}, process.env, { NODE_DEBUG: 'fetch' }) } ) const chunks = [] From e497cb45516cef9b6d19d7a4c1a1af43f6b13a07 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Sat, 30 Mar 2024 16:03:51 -0700 Subject: [PATCH 04/10] refactor: use for loop when checking NO_PROXY entries --- lib/dispatcher/env-http-proxy-agent.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index 7859b4054b4..973919c49b8 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -98,18 +98,25 @@ class EnvHttpProxyAgent extends DispatcherBase { return false // Never proxy if wildcard is set. } - return this.#noProxyEntries.every(function (entry) { + for (let i = 0; i < this.#noProxyEntries.length; i++) { + const entry = this.#noProxyEntries[i] if (entry.port && entry.port !== port) { - return true // Skip if ports don't match. + continue // Skip if ports don't match. } - if (!/^[.*]/.test(entry.hostname)) { - // No wildcards, so proxy if there is not an exact match. - return hostname !== entry.hostname + // No wildcards, so don't proxy only if there is not an exact match. + if (hostname === entry.hostname) { + return false + } + } else { + // Don't proxy if the hostname ends with the no_proxy host. + if (hostname.endsWith(entry.hostname.replace(/^\*/, ''))) { + return false + } } - // Don't proxy if the hostname ends with the no_proxy host. - return !hostname.endsWith(entry.hostname.replace(/^\*/, '')) - }) + } + + return true } } From 7384e45b753b3766ed08cf8b2febbad1e548b108 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Sat, 30 Mar 2024 16:37:56 -0700 Subject: [PATCH 05/10] feat(env-http-proxy-agent): added httpProxy, httpsProxy & noProxy options --- lib/dispatcher/env-http-proxy-agent.js | 8 ++-- test/env-http-proxy-agent.js | 53 +++++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index 973919c49b8..9dc488a65a4 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -15,26 +15,26 @@ class EnvHttpProxyAgent extends DispatcherBase { #alwaysProxy = false #noProxyEntries = [] - constructor (opts) { + constructor ({ httpProxy, httpsProxy, noProxy, ...opts } = {}) { super() this[kNoProxyAgent] = new Agent(opts) - const HTTP_PROXY = process.env.HTTP_PROXY || process.env.http_proxy + const HTTP_PROXY = httpProxy || process.env.HTTP_PROXY || process.env.http_proxy if (HTTP_PROXY) { this[kHttpProxyAgent] = new ProxyAgent({ ...opts, uri: HTTP_PROXY }) } else { this[kHttpProxyAgent] = this[kNoProxyAgent] } - const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.https_proxy + const HTTPS_PROXY = httpsProxy || process.env.HTTPS_PROXY || process.env.https_proxy if (HTTPS_PROXY) { this[kHttpsProxyAgent] = new ProxyAgent({ ...opts, uri: HTTPS_PROXY }) } else { this[kHttpsProxyAgent] = this[kHttpProxyAgent] } - const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || '' + const NO_PROXY = noProxy || process.env.NO_PROXY || process.env.no_proxy || '' this.#neverProxy = NO_PROXY === '*' this.#noProxyEntries = NO_PROXY.split(/[,\s]/) .filter(Boolean) diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index df361e15660..630b22020a0 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, describe, after, beforeEach } = require('node:test') const sinon = require('sinon') const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..') -const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed } = require('../lib/core/symbols') +const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed, kProxy } = require('../lib/core/symbols') const env = { ...process.env } @@ -29,50 +29,70 @@ test('does not create any proxy agents if HTTP_PROXY and HTTPS_PROXY are not set }) test('creates one proxy agent for both http and https when only HTTP_PROXY is defined', async (t) => { - t = tspl(t, { plan: 4 }) + t = tspl(t, { plan: 5 }) process.env.HTTP_PROXY = 'http://example.com:8080' const dispatcher = new EnvHttpProxyAgent() t.ok(dispatcher[kNoProxyAgent] instanceof Agent) t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/') t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) return dispatcher.close() }) test('creates separate proxy agent for http and https when HTTP_PROXY and HTTPS_PROXY are set', async (t) => { - t = tspl(t, { plan: 5 }) + t = tspl(t, { plan: 6 }) process.env.HTTP_PROXY = 'http://example.com:8080' process.env.HTTPS_PROXY = 'http://example.com:8443' const dispatcher = new EnvHttpProxyAgent() t.ok(dispatcher[kNoProxyAgent] instanceof Agent) t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) - t.notDeepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/') t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/') return dispatcher.close() }) test('handles lowercase http_proxy and https_proxy', async (t) => { - t = tspl(t, { plan: 5 }) + t = tspl(t, { plan: 6 }) process.env.http_proxy = 'http://example.com:8080' process.env.https_proxy = 'http://example.com:8443' const dispatcher = new EnvHttpProxyAgent() t.ok(dispatcher[kNoProxyAgent] instanceof Agent) t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) - t.notDeepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent]) + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/') + t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/') + return dispatcher.close() +}) + +test('accepts httpProxy and httpsProxy options', async (t) => { + t = tspl(t, { plan: 6 }) + const opts = { + httpProxy: 'http://example.com:8080', + httpsProxy: 'http://example.com:8443' + } + const dispatcher = new EnvHttpProxyAgent(opts) + t.ok(dispatcher[kNoProxyAgent] instanceof Agent) + t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) + t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/') t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/') return dispatcher.close() }) test('creates a proxy agent only for https when only HTTPS_PROXY is set', async (t) => { - t = tspl(t, { plan: 4 }) + t = tspl(t, { plan: 5 }) process.env.HTTPS_PROXY = 'http://example.com:8443' const dispatcher = new EnvHttpProxyAgent() t.ok(dispatcher[kNoProxyAgent] instanceof Agent) t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent)) t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent]) t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent) + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/') return dispatcher.close() }) @@ -98,7 +118,7 @@ test('destroys all agents', async (t) => { t.ok(dispatcher[kHttpsProxyAgent][kDestroyed]) }) -const createEnvHttpProxyAgentWithMocks = () => { +const createEnvHttpProxyAgentWithMocks = (opts = {}) => { const testPaths = ['/', '/example', '/32'] const mockAgent = new MockAgent() let mockPool @@ -109,7 +129,7 @@ const createEnvHttpProxyAgentWithMocks = () => { } process.env.HTTP_PROXY = 'http://localhost:8080' process.env.HTTPS_PROXY = 'http://localhost:8443' - const dispatcher = new EnvHttpProxyAgent({ factory }) + const dispatcher = new EnvHttpProxyAgent({ ...opts, factory }) const agentSymbols = [kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent] agentSymbols.forEach((agent) => { sinon.spy(dispatcher[agent], 'dispatch') @@ -200,6 +220,21 @@ describe('NO_PROXY', () => { return dispatcher.close() }) + test('as an option', async (t) => { + t = tspl(t, { plan: 9 }) + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks({ noProxy: 'example' }) + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:0')) + t.ok(await doesNotProxy('http://example:1337')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + return dispatcher.close() + }) + test('subdomain', async (t) => { t = tspl(t, { plan: 8 }) process.env.NO_PROXY = 'sub.example' From 6937a512c5112fb9ba1b39ea7923a9d282c81225 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Mon, 1 Apr 2024 22:23:29 -0700 Subject: [PATCH 06/10] feat(env-http-proxy-agent): handle changes to NO_PROXY --- lib/dispatcher/env-http-proxy-agent.js | 74 +++++++++----- test/env-http-proxy-agent.js | 131 ++++++++++++++++++++----- 2 files changed, 157 insertions(+), 48 deletions(-) diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index 9dc488a65a4..68784a67ee9 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -11,41 +11,33 @@ const DEFAULT_PORTS = { } class EnvHttpProxyAgent extends DispatcherBase { - #neverProxy = false - #alwaysProxy = false - #noProxyEntries = [] + #noProxyValue = null + #noProxyEntries = null + #opts = null - constructor ({ httpProxy, httpsProxy, noProxy, ...opts } = {}) { + constructor (opts = {}) { super() + this.#opts = opts - this[kNoProxyAgent] = new Agent(opts) + const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts - const HTTP_PROXY = httpProxy || process.env.HTTP_PROXY || process.env.http_proxy + this[kNoProxyAgent] = new Agent(agentOpts) + + const HTTP_PROXY = httpProxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy if (HTTP_PROXY) { - this[kHttpProxyAgent] = new ProxyAgent({ ...opts, uri: HTTP_PROXY }) + this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY }) } else { this[kHttpProxyAgent] = this[kNoProxyAgent] } - const HTTPS_PROXY = httpsProxy || process.env.HTTPS_PROXY || process.env.https_proxy + const HTTPS_PROXY = httpsProxy ?? process.env.HTTPS_PROXY ?? process.env.https_proxy if (HTTPS_PROXY) { - this[kHttpsProxyAgent] = new ProxyAgent({ ...opts, uri: HTTPS_PROXY }) + this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY }) } else { this[kHttpsProxyAgent] = this[kHttpProxyAgent] } - const NO_PROXY = noProxy || process.env.NO_PROXY || process.env.no_proxy || '' - this.#neverProxy = NO_PROXY === '*' - this.#noProxyEntries = NO_PROXY.split(/[,\s]/) - .filter(Boolean) - .map((entry) => { - const parsed = entry.match(/^(.+):(\d+)$/) - return { - hostname: (parsed ? parsed[1] : entry).toLowerCase(), - port: parsed ? Number.parseInt(parsed[2], 10) : 0 - } - }) - this.#alwaysProxy = this.#noProxyEntries.length === 0 + this.#parseNoProxy() } [kDispatch] (opts, handler) { @@ -91,10 +83,14 @@ class EnvHttpProxyAgent extends DispatcherBase { } #shouldProxy (hostname, port) { - if (this.#alwaysProxy) { + if (this.#noProxyChanged) { + this.#parseNoProxy() + } + + if (this.#noProxyEntries.length === 0) { return true // Always proxy if NO_PROXY is not set or empty. } - if (this.#neverProxy) { + if (this.#noProxyValue === '*') { return false // Never proxy if wildcard is set. } @@ -118,6 +114,38 @@ class EnvHttpProxyAgent extends DispatcherBase { return true } + + #parseNoProxy () { + const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv + const noProxySplit = noProxyValue.split(/[,\s]/) + const noProxyEntries = [] + + for (let i = 0; i < noProxySplit.length; i++) { + const entry = noProxySplit[i] + if (!entry) { + continue + } + const parsed = entry.match(/^(.+):(\d+)$/) + noProxyEntries.push({ + hostname: (parsed ? parsed[1] : entry).toLowerCase(), + port: parsed ? Number.parseInt(parsed[2], 10) : 0 + }) + } + + this.#noProxyValue = noProxyValue + this.#noProxyEntries = noProxyEntries + } + + get #noProxyChanged () { + if (this.#opts.noProxy !== undefined) { + return false + } + return this.#noProxyValue !== this.#noProxyEnv + } + + get #noProxyEnv () { + return process.env.NO_PROXY ?? process.env.no_proxy ?? '' + } } module.exports = EnvHttpProxyAgent diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index 630b22020a0..38c13122c26 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -84,6 +84,47 @@ test('accepts httpProxy and httpsProxy options', async (t) => { return dispatcher.close() }) +test('prefers options over env vars', async (t) => { + t = tspl(t, { plan: 2 }) + const opts = { + httpProxy: 'http://opts.example.com:8080', + httpsProxy: 'http://opts.example.com:8443' + } + process.env.HTTP_PROXY = 'http://upper.example.com:8080' + process.env.HTTPS_PROXY = 'http://upper.example.com:8443' + process.env.http_proxy = 'http://lower.example.com:8080' + process.env.https_proxy = 'http://lower.example.com:8443' + const dispatcher = new EnvHttpProxyAgent(opts) + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://opts.example.com:8080/') + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://opts.example.com:8443/') + return dispatcher.close() +}) + +test('prefers uppercase over lower case env vars', async (t) => { + t = tspl(t, { plan: 2 }) + process.env.HTTP_PROXY = 'http://upper.example.com:8080' + process.env.HTTPS_PROXY = 'http://upper.example.com:8443' + process.env.http_proxy = 'http://lower.example.com:8080' + process.env.https_proxy = 'http://lower.example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://upper.example.com:8080/') + t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://upper.example.com:8443/') + return dispatcher.close() +}) + +test('prefers uppercase over lower case env vars even when empty', async (t) => { + t = tspl(t, { plan: 2 }) + process.env.HTTP_PROXY = '' + process.env.HTTPS_PROXY = '' + process.env.http_proxy = 'http://lower.example.com:8080' + process.env.https_proxy = 'http://lower.example.com:8443' + const dispatcher = new EnvHttpProxyAgent() + + t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent]) + t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kNoProxyAgent]) + return dispatcher.close() +}) + test('creates a proxy agent only for https when only HTTPS_PROXY is set', async (t) => { t = tspl(t, { plan: 5 }) process.env.HTTPS_PROXY = 'http://example.com:8443' @@ -118,13 +159,15 @@ test('destroys all agents', async (t) => { t.ok(dispatcher[kHttpsProxyAgent][kDestroyed]) }) -const createEnvHttpProxyAgentWithMocks = (opts = {}) => { - const testPaths = ['/', '/example', '/32'] - const mockAgent = new MockAgent() - let mockPool +const createEnvHttpProxyAgentWithMocks = (plan = 1, opts = {}) => { const factory = (origin) => { - mockPool = mockAgent.get(origin) - testPaths.forEach((path) => mockPool.intercept({ path }).reply(200, 'OK')) + const mockAgent = new MockAgent() + const mockPool = mockAgent.get(origin) + let i = 0 + while (i < plan) { + mockPool.intercept({ path: /.*/ }).reply(200, 'OK') + i++ + } return mockPool } process.env.HTTP_PROXY = 'http://localhost:8080' @@ -134,16 +177,12 @@ const createEnvHttpProxyAgentWithMocks = (opts = {}) => { agentSymbols.forEach((agent) => { sinon.spy(dispatcher[agent], 'dispatch') }) - const reset = () => agentSymbols.forEach((agent) => { - dispatcher[agent].dispatch.resetHistory() - testPaths.forEach((path) => mockPool.intercept({ path }).reply(200, 'OK')) - }) const usesProxyAgent = async (agent, url) => { await fetch(url, { dispatcher }) const result = agentSymbols.every((agentSymbol) => agent === agentSymbol ? dispatcher[agentSymbol].dispatch.called : dispatcher[agentSymbol].dispatch.notCalled) - reset() + agentSymbols.forEach((agent) => { dispatcher[agent].dispatch.resetHistory() }) return result } const doesNotProxy = usesProxyAgent.bind(this, kNoProxyAgent) @@ -166,7 +205,7 @@ describe('NO_PROXY', () => { test('set to *', async (t) => { t = tspl(t, { plan: 2 }) process.env.NO_PROXY = '*' - const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks(2) t.ok(await doesNotProxy('https://example.com')) t.ok(await doesNotProxy('http://example.com')) return dispatcher.close() @@ -207,7 +246,7 @@ describe('NO_PROXY', () => { test('single host', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = 'example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9) t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) @@ -222,7 +261,7 @@ describe('NO_PROXY', () => { test('as an option', async (t) => { t = tspl(t, { plan: 9 }) - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks({ noProxy: 'example' }) + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9, { noProxy: 'example' }) t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) @@ -238,7 +277,7 @@ describe('NO_PROXY', () => { test('subdomain', async (t) => { t = tspl(t, { plan: 8 }) process.env.NO_PROXY = 'sub.example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(8) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0')) @@ -253,7 +292,7 @@ describe('NO_PROXY', () => { test('host + port', async (t) => { t = tspl(t, { plan: 12 }) process.env.NO_PROXY = 'example:80, localhost:3000' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(12) t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) @@ -272,7 +311,7 @@ describe('NO_PROXY', () => { test('host suffix', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = '.example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) @@ -288,7 +327,7 @@ describe('NO_PROXY', () => { test('host suffix with *.', async (t) => { t = tspl(t, { plan: 9 }) process.env.NO_PROXY = '*.example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) @@ -304,7 +343,7 @@ describe('NO_PROXY', () => { test('substring suffix', async (t) => { t = tspl(t, { plan: 10 }) process.env.NO_PROXY = '*example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(10) t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:1337')) @@ -321,7 +360,7 @@ describe('NO_PROXY', () => { test('arbitrary wildcards are NOT supported', async (t) => { t = tspl(t, { plan: 6 }) process.env.NO_PROXY = '.*example' - const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) @@ -334,7 +373,7 @@ describe('NO_PROXY', () => { test('IP addresses', async (t) => { t = tspl(t, { plan: 12 }) process.env.NO_PROXY = '[::1],[::2]:80,10.0.0.1,10.0.0.2:80' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(12) t.ok(await doesNotProxy('http://[::1]/')) t.ok(await doesNotProxy('http://[::1]:80/')) t.ok(await doesNotProxy('http://[::1]:1337/')) @@ -353,7 +392,7 @@ describe('NO_PROXY', () => { test('CIDR is NOT supported', async (t) => { t = tspl(t, { plan: 2 }) process.env.NO_PROXY = '127.0.0.1/32' - const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(2) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1/32')) return dispatcher.close() @@ -362,7 +401,7 @@ describe('NO_PROXY', () => { test('127.0.0.1 does NOT match localhost', async (t) => { t = tspl(t, { plan: 2 }) process.env.NO_PROXY = '127.0.0.1' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(2) t.ok(await doesNotProxy('http://127.0.0.1')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost')) return dispatcher.close() @@ -371,7 +410,7 @@ describe('NO_PROXY', () => { test('protocols that have a default port', async (t) => { t = tspl(t, { plan: 6 }) process.env.NO_PROXY = 'xxx:21,xxx:70,xxx:80,xxx:443' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) t.ok(await doesNotProxy('http://xxx')) t.ok(await doesNotProxy('http://xxx:80')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://xxx:1337')) @@ -384,7 +423,7 @@ describe('NO_PROXY', () => { test('should not be case-sensitive', async (t) => { t = tspl(t, { plan: 6 }) process.env.no_proxy = 'XXX YYY ZzZ' - const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks() + const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks(6) t.ok(await doesNotProxy('http://xxx')) t.ok(await doesNotProxy('http://XXX')) t.ok(await doesNotProxy('http://yyy')) @@ -393,4 +432,46 @@ describe('NO_PROXY', () => { t.ok(await doesNotProxy('http://zZz')) return dispatcher.close() }) + + test('prefers uppercase over lower case', async (t) => { + t = tspl(t, { plan: 2 }) + process.env.NO_PROXY = 'example.com' + process.env.no_proxy = 'sub.example.com' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) + t.ok(await doesNotProxy('http://example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + return dispatcher.close() + }) + + test('prefers uppercase over lower case even when it is empty', async (t) => { + t = tspl(t, { plan: 1 }) + process.env.NO_PROXY = '' + process.env.no_proxy = 'example.com' + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + return dispatcher.close() + }) + + test('handles env var changes', async (t) => { + t = tspl(t, { plan: 4 }) + process.env.NO_PROXY = 'example.com' + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4) + t.ok(await doesNotProxy('http://example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + process.env.NO_PROXY = 'sub.example.com' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) + t.ok(await doesNotProxy('http://sub.example.com')) + return dispatcher.close() + }) + + test('ignores env var changes when set via config', async (t) => { + t = tspl(t, { plan: 4 }) + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4, { noProxy: 'example.com' }) + t.ok(await doesNotProxy('http://example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + process.env.NO_PROXY = 'sub.example.com' + t.ok(await doesNotProxy('http://example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + return dispatcher.close() + }) }) From 8dddd82eaa6d67a6f40d18a8615bbf702709c4bb Mon Sep 17 00:00:00 2001 From: Jamie King Date: Mon, 1 Apr 2024 23:08:01 -0700 Subject: [PATCH 07/10] docs: added types for EnvHttpProxyAgent --- docs/docs/api/EnvHttpProxyAgent.md | 128 ++++++++++++++++++++++ test/types/env-http-proxy-agent.test-d.ts | 110 +++++++++++++++++++ types/env-http-proxy-agent.d.ts | 21 ++++ types/index.d.ts | 3 +- 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 docs/docs/api/EnvHttpProxyAgent.md create mode 100644 test/types/env-http-proxy-agent.test-d.ts create mode 100644 types/env-http-proxy-agent.d.ts diff --git a/docs/docs/api/EnvHttpProxyAgent.md b/docs/docs/api/EnvHttpProxyAgent.md new file mode 100644 index 00000000000..85753b2d0e2 --- /dev/null +++ b/docs/docs/api/EnvHttpProxyAgent.md @@ -0,0 +1,128 @@ +# Class: EnvHttpProxyAgent + +Extends: `undici.Dispatcher` + +EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` and sets up the proxy agents accordingly. When `HTTP_PROXY` and `HTTPS_PROXY` are set, `HTTP_PROXY` is used for HTTP requests and `HTTPS_PROXY` is used for HTTPS requests. If only `HTTP_PROXY` is set, `HTTP_PROXY` is used for both HTTP and HTTPS requests. If only `HTTPS_PROXY` is set, it is only used for HTTPS requests. + +`NO_PROXY` is a comma or space-separated list of hostnames that should not be proxied. The list may contain leading wildcard characters (`*`). If `NO_PROXY` is set, the EnvHttpProxyAgent will bypass the proxy for requests to hosts that match the list. If `NO_PROXY` is set to `"*"`, the EnvHttpProxyAgent will bypass the proxy for all requests. + +Lower case environment variables are also supported: `http_proxy`, `https_proxy`, and `no_proxy`. However, if both the lower case and upper case environment variables are set, the lower case environment variables will be ignored. + +## `new EnvHttpProxyAgent([options])` + +Arguments: + +* **options** `EnvHttpProxyAgentOptions` (optional) - extends the `Agent` options. + +Returns: `EnvHttpProxyAgent` + +### Parameter: `EnvHttpProxyAgentOptions` + +Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) + +* **httpProxy** `string` (optional) - When set, it will override the `HTTP_PROXY` environment variable. +* **httpsProxy** `string` (optional) - When set, it will override the `HTTPS_PROXY` environment variable. +* **noProxy** `string` (optional) - When set, it will override the `NO_PROXY` environment variable. + +Examples: + +```js +import { EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +// or +const envHttpProxyAgent = new EnvHttpProxyAgent({ httpProxy: 'my.proxy.server:8080', httpsProxy: 'my.proxy.server:8443', noProxy: 'localhost' }) +``` + +#### Example - EnvHttpProxyAgent instantiation + +This will instantiate the EnvHttpProxyAgent. It will not do anything until registered as the agent to use with requests. + +```js +import { EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +``` + +#### Example - Basic Proxy Request with global agent dispatcher + +```js +import { setGlobalDispatcher, request, EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +setGlobalDispatcher(envHttpProxyAgent) + +const { statusCode, body } = await request('http://localhost:3000/foo') + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + +#### Example - Basic Proxy Request with local agent dispatcher + +```js +import { EnvHttpProxyAgent, request } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() + +const { + statusCode, + body +} = await request('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + +## Instance Methods + +### `EnvHttpProxyAgent.close([callback])` + +Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). + +### `EnvHttpProxyAgent.destroy([error, callback])` + +Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). + +### `EnvHttpProxyAgent.dispatch(options, handler: AgentDispatchOptions)` + +Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). + +#### Parameter: `AgentDispatchOptions` + +Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) + +* **origin** `string | URL` +* **maxRedirections** `Integer`. + +Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). + +### `EnvHttpProxyAgent.connect(options[, callback])` + +See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). + +### `EnvHttpProxyAgent.dispatch(options, handler)` + +Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). + +### `EnvHttpProxyAgent.pipeline(options, handler)` + +See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). + +### `EnvHttpProxyAgent.request(options[, callback])` + +See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). + +### `EnvHttpProxyAgent.stream(options, factory[, callback])` + +See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). + +### `EnvHttpProxyAgent.upgrade(options[, callback])` + +See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). diff --git a/test/types/env-http-proxy-agent.test-d.ts b/test/types/env-http-proxy-agent.test-d.ts new file mode 100644 index 00000000000..c08a720e301 --- /dev/null +++ b/test/types/env-http-proxy-agent.test-d.ts @@ -0,0 +1,110 @@ +import { Duplex, Readable, Writable } from 'stream' +import { expectAssignable } from 'tsd' +import { EnvHttpProxyAgent, setGlobalDispatcher, getGlobalDispatcher, Dispatcher } from '../..' + +expectAssignable(new EnvHttpProxyAgent()) +expectAssignable(new EnvHttpProxyAgent({ httpProxy: 'http://localhost:8080', httpsProxy: 'http://localhost:8443', noProxy: 'localhost' })) + +{ + const agent = new EnvHttpProxyAgent() + expectAssignable(setGlobalDispatcher(agent)) + expectAssignable(getGlobalDispatcher()) + + // request + expectAssignable>(agent.request({ origin: '', path: '', method: 'GET' })) + expectAssignable>(agent.request({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) })) + expectAssignable>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' })) + expectAssignable(agent.request({ origin: '', path: '', method: 'GET' }, (err, data) => { + expectAssignable(err) + expectAssignable(data) + })) + expectAssignable(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }, (err, data) => { + expectAssignable(err) + expectAssignable(data) + })) + + // stream + expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET' }, data => { + expectAssignable(data) + return new Writable() + })) + expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) }, data => { + expectAssignable(data) + return new Writable() + })) + expectAssignable>(agent.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { + expectAssignable(data) + return new Writable() + })) + expectAssignable(agent.stream( + { origin: '', path: '', method: 'GET' }, + data => { + expectAssignable(data) + return new Writable() + }, + (err, data) => { + expectAssignable(err) + expectAssignable(data) + } + )) + expectAssignable(agent.stream( + { origin: new URL('http://localhost'), path: '', method: 'GET' }, + data => { + expectAssignable(data) + return new Writable() + }, + (err, data) => { + expectAssignable(err) + expectAssignable(data) + } + )) + + // pipeline + expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET' }, data => { + expectAssignable(data) + return new Readable() + })) + expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) }, data => { + expectAssignable(data) + return new Readable() + })) + expectAssignable(agent.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { + expectAssignable(data) + return new Readable() + })) + + // upgrade + expectAssignable>(agent.upgrade({ path: '' })) + expectAssignable(agent.upgrade({ path: '' }, (err, data) => { + expectAssignable(err) + expectAssignable(data) + })) + + // connect + expectAssignable>(agent.connect({ origin: '', path: '' })) + expectAssignable>(agent.connect({ origin: new URL('http://localhost'), path: '' })) + expectAssignable(agent.connect({ origin: '', path: '' }, (err, data) => { + expectAssignable(err) + expectAssignable(data) + })) + expectAssignable(agent.connect({ origin: new URL('http://localhost'), path: '' }, (err, data) => { + expectAssignable(err) + expectAssignable(data) + })) + + // dispatch + expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET' }, {})) + expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET', maxRedirections: 1 }, {})) + + // close + expectAssignable>(agent.close()) + expectAssignable(agent.close(() => {})) + + // destroy + expectAssignable>(agent.destroy()) + expectAssignable>(agent.destroy(new Error())) + expectAssignable>(agent.destroy(null)) + expectAssignable(agent.destroy(() => {})) + expectAssignable(agent.destroy(new Error(), () => {})) + expectAssignable(agent.destroy(null, () => {})) +} diff --git a/types/env-http-proxy-agent.d.ts b/types/env-http-proxy-agent.d.ts new file mode 100644 index 00000000000..d6509dc673b --- /dev/null +++ b/types/env-http-proxy-agent.d.ts @@ -0,0 +1,21 @@ +import Agent from './agent' +import Dispatcher from './dispatcher' + +export default EnvHttpProxyAgent + +declare class EnvHttpProxyAgent extends Dispatcher { + constructor(opts?: EnvHttpProxyAgent.Options) + + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; +} + +declare namespace EnvHttpProxyAgent { + export interface Options extends Agent.Options { + /** Overrides the value of the HTTP_PROXY environment variable */ + httpProxy?: string; + /** Overrides the value of the HTTPS_PROXY environment variable */ + httpsProxy?: string; + /** Overrides the value of the NO_PROXY environment variable */ + noProxy?: string; + } +} diff --git a/types/index.d.ts b/types/index.d.ts index 05f01ea1f4d..08f0684d96d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,6 +14,7 @@ import MockPool from'./mock-pool' import MockAgent from'./mock-agent' import mockErrors from'./mock-errors' import ProxyAgent from'./proxy-agent' +import EnvHttpProxyAgent from './env-http-proxy-agent' import RetryHandler from'./retry-handler' import RetryAgent from'./retry-agent' import { request, pipeline, stream, connect, upgrade } from './api' @@ -31,7 +32,7 @@ export * from './content-type' export * from './cache' export { Interceptable } from './mock-interceptor' -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } export default Undici declare namespace Undici { From f85f8254539fa6d9a18df1f16ca8f2faa2f67174 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Fri, 19 Apr 2024 06:31:37 -0700 Subject: [PATCH 08/10] test: resolve windows issues, mark experimental, update doc --- docs/docs/api/EnvHttpProxyAgent.md | 38 ++++++++++++++++++++++++++ lib/dispatcher/env-http-proxy-agent.js | 9 ++++++ test/env-http-proxy-agent.js | 16 +++++------ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/docs/docs/api/EnvHttpProxyAgent.md b/docs/docs/api/EnvHttpProxyAgent.md index 85753b2d0e2..fce9c0d4736 100644 --- a/docs/docs/api/EnvHttpProxyAgent.md +++ b/docs/docs/api/EnvHttpProxyAgent.md @@ -1,5 +1,7 @@ # Class: EnvHttpProxyAgent +Stability: Experimental. + Extends: `undici.Dispatcher` EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` and sets up the proxy agents accordingly. When `HTTP_PROXY` and `HTTPS_PROXY` are set, `HTTP_PROXY` is used for HTTP requests and `HTTPS_PROXY` is used for HTTPS requests. If only `HTTP_PROXY` is set, `HTTP_PROXY` is used for both HTTP and HTTPS requests. If only `HTTPS_PROXY` is set, it is only used for HTTPS requests. @@ -44,6 +46,23 @@ import { EnvHttpProxyAgent } from 'undici' const envHttpProxyAgent = new EnvHttpProxyAgent() ``` +#### Example - Basic Proxy Fetch with global agent dispatcher + +```js +import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +setGlobalDispatcher(envHttpProxyAgent) + +const { statusCode, body } = await fetch('http://localhost:3000/foo') + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + #### Example - Basic Proxy Request with global agent dispatcher ```js @@ -75,11 +94,30 @@ const { console.log('response received', statusCode) // response received 200 +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} + +```#### Example - Basic Proxy Fetch with local agent dispatcher + +```js +import { EnvHttpProxyAgent, fetch } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() + +const { + statusCode, + body +} = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) + +console.log('response received', statusCode) // response received 200 + for await (const data of body) { console.log('data', data.toString('utf8')) // data foo } ``` + ## Instance Methods ### `EnvHttpProxyAgent.close([callback])` diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index 68784a67ee9..37c526df280 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -10,6 +10,8 @@ const DEFAULT_PORTS = { 'https:': 443 } +let experimentalWarned = false + class EnvHttpProxyAgent extends DispatcherBase { #noProxyValue = null #noProxyEntries = null @@ -19,6 +21,13 @@ class EnvHttpProxyAgent extends DispatcherBase { super() this.#opts = opts + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('EnvHttpProxyAgent is experimental, expect them to change at any time.', { + code: 'UNDICI-EHPA' + }) + } + const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts this[kNoProxyAgent] = new Agent(agentOpts) diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index 38c13122c26..3483fa5de7c 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -90,10 +90,10 @@ test('prefers options over env vars', async (t) => { httpProxy: 'http://opts.example.com:8080', httpsProxy: 'http://opts.example.com:8443' } - process.env.HTTP_PROXY = 'http://upper.example.com:8080' - process.env.HTTPS_PROXY = 'http://upper.example.com:8443' process.env.http_proxy = 'http://lower.example.com:8080' process.env.https_proxy = 'http://lower.example.com:8443' + process.env.HTTP_PROXY = 'http://upper.example.com:8080' + process.env.HTTPS_PROXY = 'http://upper.example.com:8443' const dispatcher = new EnvHttpProxyAgent(opts) t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://opts.example.com:8080/') t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://opts.example.com:8443/') @@ -102,10 +102,10 @@ test('prefers options over env vars', async (t) => { test('prefers uppercase over lower case env vars', async (t) => { t = tspl(t, { plan: 2 }) - process.env.HTTP_PROXY = 'http://upper.example.com:8080' - process.env.HTTPS_PROXY = 'http://upper.example.com:8443' process.env.http_proxy = 'http://lower.example.com:8080' process.env.https_proxy = 'http://lower.example.com:8443' + process.env.HTTP_PROXY = 'http://upper.example.com:8080' + process.env.HTTPS_PROXY = 'http://upper.example.com:8443' const dispatcher = new EnvHttpProxyAgent() t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://upper.example.com:8080/') t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://upper.example.com:8443/') @@ -114,10 +114,10 @@ test('prefers uppercase over lower case env vars', async (t) => { test('prefers uppercase over lower case env vars even when empty', async (t) => { t = tspl(t, { plan: 2 }) - process.env.HTTP_PROXY = '' - process.env.HTTPS_PROXY = '' process.env.http_proxy = 'http://lower.example.com:8080' process.env.https_proxy = 'http://lower.example.com:8443' + process.env.HTTP_PROXY = '' + process.env.HTTPS_PROXY = '' const dispatcher = new EnvHttpProxyAgent() t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent]) @@ -435,8 +435,8 @@ describe('NO_PROXY', () => { test('prefers uppercase over lower case', async (t) => { t = tspl(t, { plan: 2 }) - process.env.NO_PROXY = 'example.com' process.env.no_proxy = 'sub.example.com' + process.env.NO_PROXY = 'example.com' const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) t.ok(await doesNotProxy('http://example.com')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) @@ -445,8 +445,8 @@ describe('NO_PROXY', () => { test('prefers uppercase over lower case even when it is empty', async (t) => { t = tspl(t, { plan: 1 }) - process.env.NO_PROXY = '' process.env.no_proxy = 'example.com' + process.env.NO_PROXY = '' const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks() t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) return dispatcher.close() From f42599ad4b6825ef0831d2683f5b992d6979467c Mon Sep 17 00:00:00 2001 From: Jamie King Date: Fri, 19 Apr 2024 06:34:48 -0700 Subject: [PATCH 09/10] docs: fix typo --- docs/docs/api/EnvHttpProxyAgent.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/api/EnvHttpProxyAgent.md b/docs/docs/api/EnvHttpProxyAgent.md index fce9c0d4736..54c44ffe0e5 100644 --- a/docs/docs/api/EnvHttpProxyAgent.md +++ b/docs/docs/api/EnvHttpProxyAgent.md @@ -97,8 +97,9 @@ console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // data foo } +``` -```#### Example - Basic Proxy Fetch with local agent dispatcher +#### Example - Basic Proxy Fetch with local agent dispatcher ```js import { EnvHttpProxyAgent, fetch } from 'undici' @@ -117,7 +118,6 @@ for await (const data of body) { } ``` - ## Instance Methods ### `EnvHttpProxyAgent.close([callback])` From c322c33dcf6d97bfb0ff3490792791c4d3ae2021 Mon Sep 17 00:00:00 2001 From: Jamie King Date: Fri, 19 Apr 2024 06:41:01 -0700 Subject: [PATCH 10/10] docs: fetch for EnvHttpProxyAgent --- docs/docs/api/EnvHttpProxyAgent.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/docs/api/EnvHttpProxyAgent.md b/docs/docs/api/EnvHttpProxyAgent.md index 54c44ffe0e5..b1ee39b995d 100644 --- a/docs/docs/api/EnvHttpProxyAgent.md +++ b/docs/docs/api/EnvHttpProxyAgent.md @@ -54,13 +54,11 @@ import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici' const envHttpProxyAgent = new EnvHttpProxyAgent() setGlobalDispatcher(envHttpProxyAgent) -const { statusCode, body } = await fetch('http://localhost:3000/foo') +const { status, json } = await fetch('http://localhost:3000/foo') -console.log('response received', statusCode) // response received 200 +console.log('response received', status) // response received 200 -for await (const data of body) { - console.log('data', data.toString('utf8')) // data foo -} +const data = await json() // data { foo: "bar" } ``` #### Example - Basic Proxy Request with global agent dispatcher @@ -107,15 +105,13 @@ import { EnvHttpProxyAgent, fetch } from 'undici' const envHttpProxyAgent = new EnvHttpProxyAgent() const { - statusCode, - body + status, + json } = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) -console.log('response received', statusCode) // response received 200 +console.log('response received', status) // response received 200 -for await (const data of body) { - console.log('data', data.toString('utf8')) // data foo -} +const data = await json() // data { foo: "bar" } ``` ## Instance Methods