http: add http.setGlobalProxyFromEnv() · nodejs/node@8470e29 · GitHub
Skip to content

Commit 8470e29

Browse files
joyeecheungRafaelGSS
authored andcommitted
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. PR-URL: #60953 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 42666c2 commit 8470e29

25 files changed

Lines changed: 807 additions & 44 deletions

doc/api/http.md

Lines changed: 110 additions & 0 deletions

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: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ const {
2525
ObjectDefineProperty,
2626
} = primordials;
2727

28-
const { validateInteger } = require('internal/validators');
28+
const { validateInteger, validateObject } = require('internal/validators');
2929
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,58 @@ function lazyUndici() {
123129
return undici ??= require('internal/deps/undici/undici');
124130
}
125131

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

147206
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Tests that http.setGlobalProxyFromEnv() without arguments uses process.env.
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, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
8+
httpEndpoint: true,
9+
});
10+
11+
// Test that calling setGlobalProxyFromEnv() without arguments uses process.env
12+
await checkProxiedFetch({
13+
FETCH_URL: requestUrl,
14+
// Set the proxy in the environment instead of passing it to SET_GLOBAL_PROXY
15+
http_proxy: proxyUrl,
16+
SET_GLOBAL_PROXY_DEFAULT: '1', // Signal to call without arguments
17+
}, {
18+
stdout: 'Hello world',
19+
});
20+
21+
shutdown();
22+
23+
// FIXME(undici:4083): undici currently always tunnels the request over
24+
// CONNECT if proxyTunnel is not explicitly set to false.
25+
const expectedLogs = [{
26+
method: 'CONNECT',
27+
url: serverHost,
28+
headers: {
29+
'connection': 'close',
30+
'host': serverHost,
31+
'proxy-connection': 'keep-alive',
32+
},
33+
}];
34+
35+
assert.deepStrictEqual(proxyLogs, expectedLogs);
Lines changed: 21 additions & 0 deletions

0 commit comments

Comments
 (0)