Skip to content

Commit cc30706

Browse files
committed
http: add http.setGlobalProxyFromEnv()
This adds an API to dynamically enable built-in proxy support for all of fetch() and http.request()/https.request(), so that users do not have to be aware of them all and configure them one by one.
1 parent 85852a3 commit cc30706

22 files changed

+696
-43
lines changed

doc/api/http.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4361,6 +4361,31 @@ added:
43614361
43624362
Set the maximum number of idle HTTP parsers.
43634363
4364+
## `http.setGlobalProxyFromEnv(proxyEnv)`
4365+
4366+
<!-- YAML
4367+
added:
4368+
- REPLACEME
4369+
-->
4370+
4371+
* `proxyEnv` {Object} An object containing proxy configuration. This accepts the
4372+
same options as the `proxyEnv` option accepted by [`Agent`][]
4373+
* Returns: {Function} A function that restores the original agent and dispatcher
4374+
settings to the state before this `http.setGlobalProxyFromEnv()` is invoked.
4375+
4376+
Dynamically resets the global configurations to enable built-in proxy support for
4377+
`fetch()` and `http.request()`/`https.request()` at runtime, as an alternative
4378+
to using the `--use-env-proxy` flag or `NODE_USE_ENV_PROXY` environment variable.
4379+
It can also be used to override settings configured from the environment variables.
4380+
4381+
As this function resets the global configurations, any previously configured
4382+
`http.globalAgent`, `https.globalAgent` or undici global dispatcher would be
4383+
overridden after this function is invoked. It's recommended to invoke it before any
4384+
requests are made and avoid invoking it in the middle of any requests.
4385+
4386+
See [Built-in Proxy Support][] for details on proxy URL formats and `NO_PROXY`
4387+
syntax.
4388+
43644389
## Class: `WebSocket`
43654390
43664391
<!-- YAML
@@ -4383,6 +4408,9 @@ added:
43834408
When Node.js creates the global agent, if the `NODE_USE_ENV_PROXY` environment variable is
43844409
set to `1` or `--use-env-proxy` is enabled, the global agent will be constructed
43854410
with `proxyEnv: process.env`, enabling proxy support based on the environment variables.
4411+
4412+
To enable proxy support dynamically and globally, use [`http.setGlobalProxyFromEnv()`][].
4413+
43864414
Custom agents can also be created with proxy support by passing a
43874415
`proxyEnv` option when constructing the agent. The value can be `process.env`
43884416
if they just want to inherit the configuration from the environment variables,
@@ -4438,6 +4466,65 @@ Or the `--use-env-proxy` flag.
44384466
HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node --use-env-proxy client.js
44394467
```
44404468
4469+
To enable proxy support dynamically and globally:
4470+
4471+
```cjs
4472+
const http = require('node:http');
4473+
4474+
const restore = http.setGlobalProxyFromEnv({
4475+
http_proxy: 'http://proxy.example.com:8080',
4476+
https_proxy: 'https://proxy.example.com:8443',
4477+
no_proxy: 'localhost,127.0.0.1,.internal.example.com',
4478+
});
4479+
4480+
// Subsequent requests will use the configured proxies
4481+
http.get('http://www.example.com', (res) => {
4482+
// This request will be proxied through proxy.example.com:8080
4483+
});
4484+
4485+
fetch('https://www.example.com', (res) => {
4486+
// This request will be proxied through proxy.example.com:8443
4487+
});
4488+
```
4489+
4490+
```mjs
4491+
import http from 'node:http';
4492+
4493+
http.setGlobalProxyFromEnv({
4494+
http_proxy: 'http://proxy.example.com:8080',
4495+
https_proxy: 'https://proxy.example.com:8443',
4496+
no_proxy: 'localhost,127.0.0.1,.internal.example.com',
4497+
});
4498+
4499+
// Subsequent requests will use the configured proxies
4500+
http.get('http://www.example.com', (res) => {
4501+
// This request will be proxied through proxy.example.com:8080
4502+
});
4503+
4504+
fetch('https://www.example.com', (res) => {
4505+
// This request will be proxied through proxy.example.com:8443
4506+
});
4507+
```
4508+
4509+
To clear the dynamically enabled global proxy configuration:
4510+
4511+
```cjs
4512+
const http = require('node:http');
4513+
const restore = http.setGlobalProxyFromEnv({ /* ... */ });
4514+
// Perform requests that will be proxied.
4515+
restore();
4516+
// From now on, requests will no longer be proxied.
4517+
```
4518+
4519+
```mjs
4520+
import http from 'node:http';
4521+
4522+
const restore = http.setGlobalProxyFromEnv({ /* ... */ });
4523+
// Perform requests that will be proxied.
4524+
restore();
4525+
// From now on, requests will no longer be proxied.
4526+
```
4527+
44414528
To create a custom agent with built-in proxy support:
44424529
44434530
```cjs
@@ -4501,6 +4588,7 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
45014588
[`http.get()`]: #httpgetoptions-callback
45024589
[`http.globalAgent`]: #httpglobalagent
45034590
[`http.request()`]: #httprequestoptions-callback
4591+
[`http.setGlobalProxyFromEnv()`]: #httpsetglobalproxyfromenvproxyenv
45044592
[`message.headers`]: #messageheaders
45054593
[`message.rawHeaders`]: #messagerawheaders
45064594
[`message.socket`]: #messagesocket

lib/_http_agent.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const {
4040
kProxyConfig,
4141
checkShouldUseProxy,
4242
kWaitForProxyTunnel,
43-
filterEnvForProxies,
43+
getGlobalAgent,
4444
} = require('internal/http');
4545
const { AsyncResource } = require('async_hooks');
4646
const { async_id_symbol } = require('internal/async_hooks').symbols;
@@ -627,9 +627,5 @@ function asyncResetHandle(socket) {
627627

628628
module.exports = {
629629
Agent,
630-
globalAgent: new Agent({
631-
keepAlive: true, scheduling: 'lifo', timeout: 5000,
632-
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
633-
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
634-
}),
630+
globalAgent: getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent),
635631
};

lib/http.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const httpAgent = require('_http_agent');
3030
const { ClientRequest } = require('_http_client');
3131
const { methods, parsers } = require('_http_common');
3232
const { IncomingMessage } = require('_http_incoming');
33+
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
3334
const {
3435
validateHeaderName,
3536
validateHeaderValue,
@@ -41,6 +42,11 @@ const {
4142
Server,
4243
ServerResponse,
4344
} = require('_http_server');
45+
const {
46+
parseProxyUrl,
47+
getGlobalAgent,
48+
} = require('internal/http');
49+
const { URL } = require('internal/url');
4450
let maxHeaderSize;
4551
let undici;
4652

@@ -123,6 +129,57 @@ function lazyUndici() {
123129
return undici ??= require('internal/deps/undici/undici');
124130
}
125131

132+
function setGlobalProxyFromEnv(env) {
133+
const httpProxy = parseProxyUrl(env, 'http:');
134+
const httpsProxy = parseProxyUrl(env, 'https:');
135+
const noProxy = env.no_proxy || env.NO_PROXY;
136+
137+
if (!httpProxy && !httpsProxy) {
138+
return () => {};
139+
}
140+
141+
if (httpProxy && !URL.canParse(httpProxy)) {
142+
throw new ERR_PROXY_INVALID_CONFIG(httpProxy);
143+
}
144+
if (httpsProxy && !URL.canParse(httpsProxy)) {
145+
throw new ERR_PROXY_INVALID_CONFIG(httpsProxy);
146+
}
147+
148+
let originalDispatcher, originalHttpsAgent, originalHttpAgent;
149+
if (httpProxy || httpsProxy) {
150+
// Set it for fetch.
151+
const { setGlobalDispatcher, getGlobalDispatcher, EnvHttpProxyAgent } = lazyUndici();
152+
const envHttpProxyAgent = new EnvHttpProxyAgent({
153+
__proto__: null, httpProxy, httpsProxy, noProxy,
154+
});
155+
originalDispatcher = getGlobalDispatcher();
156+
setGlobalDispatcher(envHttpProxyAgent);
157+
}
158+
159+
if (httpProxy) {
160+
originalHttpAgent = module.exports.globalAgent;
161+
module.exports.globalAgent = getGlobalAgent(env, httpAgent.Agent);
162+
}
163+
if (httpsProxy && !!process.versions.openssl) {
164+
const https = require('https');
165+
originalHttpsAgent = https.globalAgent;
166+
https.globalAgent = getGlobalAgent(env, https.Agent);
167+
}
168+
169+
return function restore() {
170+
if (originalDispatcher) {
171+
const { setGlobalDispatcher } = lazyUndici();
172+
setGlobalDispatcher(originalDispatcher);
173+
}
174+
if (originalHttpAgent) {
175+
module.exports.globalAgent = originalHttpAgent;
176+
}
177+
if (originalHttpsAgent) {
178+
require('https').globalAgent = originalHttpsAgent;
179+
}
180+
};
181+
}
182+
126183
module.exports = {
127184
_connectionListener,
128185
METHODS: methods.toSorted(),
@@ -142,6 +199,7 @@ module.exports = {
142199
validateInteger(max, 'max', 1);
143200
parsers.max = max;
144201
},
202+
setGlobalProxyFromEnv,
145203
};
146204

147205
ObjectDefineProperty(module.exports, 'maxHeaderSize', {

lib/https.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ const tls = require('tls');
5050
const {
5151
kProxyConfig,
5252
checkShouldUseProxy,
53-
filterEnvForProxies,
5453
kWaitForProxyTunnel,
54+
getGlobalAgent,
5555
} = require('internal/http');
5656
const { Agent: HttpAgent } = require('_http_agent');
5757
const {
@@ -602,11 +602,7 @@ Agent.prototype._evictSession = function _evictSession(key) {
602602
delete this._sessionCache.map[key];
603603
};
604604

605-
const globalAgent = new Agent({
606-
keepAlive: true, scheduling: 'lifo', timeout: 5000,
607-
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
608-
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
609-
});
605+
const globalAgent = getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent);
610606

611607
/**
612608
* Makes a request to a secure web server.

lib/internal/http.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,7 @@ class ProxyConfig {
186186
}
187187
}
188188

189-
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
190-
// We only support proxying for HTTP and HTTPS requests.
191-
if (protocol !== 'http:' && protocol !== 'https:') {
192-
return null;
193-
}
189+
function parseProxyUrl(env, protocol) {
194190
// Get the proxy url - following the most popular convention, lower case takes precedence.
195191
// See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy
196192
const proxyUrl = (protocol === 'https:') ?
@@ -204,6 +200,20 @@ function parseProxyConfigFromEnv(env, protocol, keepAlive) {
204200
throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
205201
}
206202

203+
return proxyUrl;
204+
}
205+
206+
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
207+
// We only support proxying for HTTP and HTTPS requests.
208+
if (protocol !== 'http:' && protocol !== 'https:') {
209+
return null;
210+
}
211+
212+
const proxyUrl = parseProxyUrl(env, protocol);
213+
if (proxyUrl === null) {
214+
return null;
215+
}
216+
207217
// Only http:// and https:// proxies are supported.
208218
// Ignore instead of throw, in case other protocols are supposed to be
209219
// handled by the user land.
@@ -244,6 +254,13 @@ function filterEnvForProxies(env) {
244254
};
245255
}
246256

257+
function getGlobalAgent(proxyEnv, Agent) {
258+
return new Agent({
259+
keepAlive: true, scheduling: 'lifo', timeout: 5000,
260+
proxyEnv,
261+
});
262+
}
263+
247264
module.exports = {
248265
kOutHeaders: Symbol('kOutHeaders'),
249266
kNeedDrain: Symbol('kNeedDrain'),
@@ -257,4 +274,6 @@ module.exports = {
257274
getNextTraceEventId,
258275
isTraceHTTPEnabled,
259276
filterEnvForProxies,
277+
getGlobalAgent,
278+
parseProxyUrl,
260279
};

test/client-proxy/test-http-proxy-fetch.mjs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ await once(proxy, 'listening');
2020
const serverHost = `localhost:${server.address().port}`;
2121

2222
// FIXME(undici:4083): undici currently always tunnels the request over
23-
// CONNECT if proxyTunnel is not explicitly set to false, but what we
24-
// need is for it to be automatically false for HTTP requests to be
25-
// consistent with curl.
23+
// CONNECT if proxyTunnel is not explicitly set to false.
2624
const expectedLogs = [{
2725
method: 'CONNECT',
2826
url: serverHost,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Tests that http.setGlobalProxyFromEnv() is a no-op when no proxy is configured.
2+
3+
import '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
6+
7+
const { proxyLogs, shutdown, httpEndpoint: { requestUrl } } = await startTestServers({
8+
httpEndpoint: true,
9+
});
10+
11+
await checkProxiedFetch({
12+
FETCH_URL: requestUrl,
13+
SET_GLOBAL_PROXY: JSON.stringify({}),
14+
}, {
15+
stdout: 'Hello world',
16+
});
17+
18+
shutdown();
19+
20+
// Verify request did NOT go through proxy.
21+
assert.deepStrictEqual(proxyLogs, []);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Tests that http.setGlobalProxyFromEnv() works with fetch().
2+
3+
import * as common from '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import * as fixtures from '../common/fixtures.mjs';
6+
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
7+
8+
if (!common.hasCrypto) {
9+
common.skip('Needs crypto');
10+
}
11+
12+
const { proxyLogs, proxyUrl, shutdown, httpsEndpoint: { serverHost, requestUrl } } = await startTestServers({
13+
httpsEndpoint: true,
14+
});
15+
16+
await checkProxiedFetch({
17+
FETCH_URL: requestUrl,
18+
SET_GLOBAL_PROXY: JSON.stringify({
19+
https_proxy: proxyUrl,
20+
}),
21+
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
22+
}, {
23+
stdout: 'Hello world',
24+
});
25+
26+
shutdown();
27+
28+
const expectedLogs = [{
29+
method: 'CONNECT',
30+
url: serverHost,
31+
headers: {
32+
'connection': 'close',
33+
'host': serverHost,
34+
'proxy-connection': 'keep-alive',
35+
},
36+
}];
37+
38+
assert.deepStrictEqual(proxyLogs, expectedLogs);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Tests that http.setGlobalProxyFromEnv() works with no_proxy configuration.
2+
3+
import '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
6+
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { requestUrl } } = await startTestServers({
7+
httpEndpoint: true,
8+
});
9+
10+
await checkProxiedFetch({
11+
FETCH_URL: requestUrl,
12+
SET_GLOBAL_PROXY: JSON.stringify({
13+
http_proxy: proxyUrl,
14+
no_proxy: 'localhost',
15+
}),
16+
}, {
17+
stdout: 'Hello world',
18+
});
19+
20+
shutdown();
21+
22+
// Verify request did NOT go through proxy.
23+
assert.deepStrictEqual(proxyLogs, []);

0 commit comments

Comments
 (0)