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: https://github.com/nodejs/node/pull/60953
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
This commit is contained in:
Joyee Cheung authored and GitHub committed 2025-12-10 16:12:28 +00:00
1 parent 32ea48d749
commit c0108bfc55
25 files changed
+807 -44

No files matched your search

+110
View File
@@ -4361,6 +4361,32 @@ added:
Set the maximum number of idle HTTP parsers.
## `http.setGlobalProxyFromEnv([proxyEnv])`
<!-- YAML
added:
- REPLACEME
-->
* `proxyEnv` {Object} An object containing proxy configuration. This accepts the
same options as the `proxyEnv` option accepted by [`Agent`][]. **Default:**
`process.env`.
* Returns: {Function} A function that restores the original agent and dispatcher
settings to the state before this `http.setGlobalProxyFromEnv()` is invoked.
Dynamically resets the global configurations to enable built-in proxy support for
`fetch()` and `http.request()`/`https.request()` at runtime, as an alternative
to using the `--use-env-proxy` flag or `NODE_USE_ENV_PROXY` environment variable.
It can also be used to override settings configured from the environment variables.
As this function resets the global configurations, any previously configured
`http.globalAgent`, `https.globalAgent` or undici global dispatcher would be
overridden after this function is invoked. It's recommended to invoke it before any
requests are made and avoid invoking it in the middle of any requests.
See [Built-in Proxy Support][] for details on proxy URL formats and `NO_PROXY`
syntax.
## Class: `WebSocket`
<!-- YAML
@@ -4383,6 +4409,9 @@ added:
When Node.js creates the global agent, if the `NODE_USE_ENV_PROXY` environment variable is
set to `1` or `--use-env-proxy` is enabled, the global agent will be constructed
with `proxyEnv: process.env`, enabling proxy support based on the environment variables.
To enable proxy support dynamically and globally, use [`http.setGlobalProxyFromEnv()`][].
Custom agents can also be created with proxy support by passing a
`proxyEnv` option when constructing the agent. The value can be `process.env`
if they just want to inherit the configuration from the environment variables,
@@ -4438,6 +4467,86 @@ Or the `--use-env-proxy` flag.
HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node --use-env-proxy client.js
```
To enable proxy support dynamically and globally with `process.env` (the default option of `http.setGlobalProxyFromEnv()`):
```cjs
const http = require('node:http');
// Reads proxy-related environment variables from process.env
const restore = http.setGlobalProxyFromEnv();
// Subsequent requests will use the configured proxies from environment variables
http.get('http://www.example.com', (res) => {
// This request will be proxied if HTTP_PROXY or http_proxy is set
});
fetch('https://www.example.com', (res) => {
// This request will be proxied if HTTPS_PROXY or https_proxy is set
});
// To restore the original global agent and dispatcher settings, call the returned function.
// restore();
```
```mjs
import http from 'node:http';
// Reads proxy-related environment variables from process.env
http.setGlobalProxyFromEnv();
// Subsequent requests will use the configured proxies from environment variables
http.get('http://www.example.com', (res) => {
// This request will be proxied if HTTP_PROXY or http_proxy is set
});
fetch('https://www.example.com', (res) => {
// This request will be proxied if HTTPS_PROXY or https_proxy is set
});
// To restore the original global agent and dispatcher settings, call the returned function.
// restore();
```
To enable proxy support dynamically and globally with custom settings:
```cjs
const http = require('node:http');
const restore = http.setGlobalProxyFromEnv({
http_proxy: 'http://proxy.example.com:8080',
https_proxy: 'https://proxy.example.com:8443',
no_proxy: 'localhost,127.0.0.1,.internal.example.com',
});
// Subsequent requests will use the configured proxies
http.get('http://www.example.com', (res) => {
// This request will be proxied through proxy.example.com:8080
});
fetch('https://www.example.com', (res) => {
// This request will be proxied through proxy.example.com:8443
});
```
```mjs
import http from 'node:http';
http.setGlobalProxyFromEnv({
http_proxy: 'http://proxy.example.com:8080',
https_proxy: 'https://proxy.example.com:8443',
no_proxy: 'localhost,127.0.0.1,.internal.example.com',
});
// Subsequent requests will use the configured proxies
http.get('http://www.example.com', (res) => {
// This request will be proxied through proxy.example.com:8080
});
fetch('https://www.example.com', (res) => {
// This request will be proxied through proxy.example.com:8443
});
```
To create a custom agent with built-in proxy support:
```cjs
@@ -4501,6 +4610,7 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
[`http.get()`]: #httpgetoptions-callback
[`http.globalAgent`]: #httpglobalagent
[`http.request()`]: #httprequestoptions-callback
[`http.setGlobalProxyFromEnv()`]: #httpsetglobalproxyfromenvproxyenv
[`message.headers`]: #messageheaders
[`message.rawHeaders`]: #messagerawheaders
[`message.socket`]: #messagesocket
+2 -6
View File
@@ -40,7 +40,7 @@ const {
kProxyConfig,
checkShouldUseProxy,
kWaitForProxyTunnel,
filterEnvForProxies,
getGlobalAgent,
} = require('internal/http');
const { AsyncResource } = require('async_hooks');
const { async_id_symbol } = require('internal/async_hooks').symbols;
@@ -627,9 +627,5 @@ function asyncResetHandle(socket) {
module.exports = {
Agent,
globalAgent: new Agent({
keepAlive: true, scheduling: 'lifo', timeout: 5000,
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
}),
globalAgent: getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent),
};
+60 -1
View File
@@ -25,11 +25,12 @@ const {
ObjectDefineProperty,
} = primordials;
const { validateInteger } = require('internal/validators');
const { validateInteger, validateObject } = require('internal/validators');
const httpAgent = require('_http_agent');
const { ClientRequest } = require('_http_client');
const { methods, parsers } = require('_http_common');
const { IncomingMessage } = require('_http_incoming');
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
const {
validateHeaderName,
validateHeaderValue,
@@ -41,6 +42,11 @@ const {
Server,
ServerResponse,
} = require('_http_server');
const {
parseProxyUrl,
getGlobalAgent,
} = require('internal/http');
const { URL } = require('internal/url');
let maxHeaderSize;
let undici;
@@ -123,6 +129,58 @@ function lazyUndici() {
return undici ??= require('internal/deps/undici/undici');
}
function setGlobalProxyFromEnv(env = process.env) {
validateObject(env, 'proxyEnv');
const httpProxy = parseProxyUrl(env, 'http:');
const httpsProxy = parseProxyUrl(env, 'https:');
const noProxy = env.no_proxy || env.NO_PROXY;
if (!httpProxy && !httpsProxy) {
return () => {};
}
if (httpProxy && !URL.canParse(httpProxy)) {
throw new ERR_PROXY_INVALID_CONFIG(httpProxy);
}
if (httpsProxy && !URL.canParse(httpsProxy)) {
throw new ERR_PROXY_INVALID_CONFIG(httpsProxy);
}
let originalDispatcher, originalHttpsAgent, originalHttpAgent;
if (httpProxy || httpsProxy) {
// Set it for fetch.
const { setGlobalDispatcher, getGlobalDispatcher, EnvHttpProxyAgent } = lazyUndici();
const envHttpProxyAgent = new EnvHttpProxyAgent({
__proto__: null, httpProxy, httpsProxy, noProxy,
});
originalDispatcher = getGlobalDispatcher();
setGlobalDispatcher(envHttpProxyAgent);
}
if (httpProxy) {
originalHttpAgent = module.exports.globalAgent;
module.exports.globalAgent = getGlobalAgent(env, httpAgent.Agent);
}
if (httpsProxy && !!process.versions.openssl) {
const https = require('https');
originalHttpsAgent = https.globalAgent;
https.globalAgent = getGlobalAgent(env, https.Agent);
}
return function restore() {
if (originalDispatcher) {
const { setGlobalDispatcher } = lazyUndici();
setGlobalDispatcher(originalDispatcher);
}
if (originalHttpAgent) {
module.exports.globalAgent = originalHttpAgent;
}
if (originalHttpsAgent) {
require('https').globalAgent = originalHttpsAgent;
}
};
}
module.exports = {
_connectionListener,
METHODS: methods.toSorted(),
@@ -142,6 +200,7 @@ module.exports = {
validateInteger(max, 'max', 1);
parsers.max = max;
},
setGlobalProxyFromEnv,
};
ObjectDefineProperty(module.exports, 'maxHeaderSize', {
+2 -6
View File
@@ -50,8 +50,8 @@ const tls = require('tls');
const {
kProxyConfig,
checkShouldUseProxy,
filterEnvForProxies,
kWaitForProxyTunnel,
getGlobalAgent,
} = require('internal/http');
const { Agent: HttpAgent } = require('_http_agent');
const {
@@ -602,11 +602,7 @@ Agent.prototype._evictSession = function _evictSession(key) {
delete this._sessionCache.map[key];
};
const globalAgent = new Agent({
keepAlive: true, scheduling: 'lifo', timeout: 5000,
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
});
const globalAgent = getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent);
/**
* Makes a request to a secure web server.
+24 -5
View File
@@ -186,11 +186,7 @@ class ProxyConfig {
}
}
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
// We only support proxying for HTTP and HTTPS requests.
if (protocol !== 'http:' && protocol !== 'https:') {
return null;
}
function parseProxyUrl(env, protocol) {
// Get the proxy url - following the most popular convention, lower case takes precedence.
// See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy
const proxyUrl = (protocol === 'https:') ?
@@ -204,6 +200,20 @@ function parseProxyConfigFromEnv(env, protocol, keepAlive) {
throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
}
return proxyUrl;
}
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
// We only support proxying for HTTP and HTTPS requests.
if (protocol !== 'http:' && protocol !== 'https:') {
return null;
}
const proxyUrl = parseProxyUrl(env, protocol);
if (proxyUrl === null) {
return null;
}
// Only http:// and https:// proxies are supported.
// Ignore instead of throw, in case other protocols are supposed to be
// handled by the user land.
@@ -244,6 +254,13 @@ function filterEnvForProxies(env) {
};
}
function getGlobalAgent(proxyEnv, Agent) {
return new Agent({
keepAlive: true, scheduling: 'lifo', timeout: 5000,
proxyEnv,
});
}
module.exports = {
kOutHeaders: Symbol('kOutHeaders'),
kNeedDrain: Symbol('kNeedDrain'),
@@ -257,4 +274,6 @@ module.exports = {
getNextTraceEventId,
isTraceHTTPEnabled,
filterEnvForProxies,
getGlobalAgent,
parseProxyUrl,
};
+1 -3
View File
@@ -20,9 +20,7 @@ await once(proxy, 'listening');
const serverHost = `localhost:${server.address().port}`;
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false, but what we
// need is for it to be automatically false for HTTP requests to be
// consistent with curl.
// CONNECT if proxyTunnel is not explicitly set to false.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
@@ -0,0 +1,35 @@
// Tests that http.setGlobalProxyFromEnv() without arguments uses process.env.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
const { proxyLogs, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
// Test that calling setGlobalProxyFromEnv() without arguments uses process.env
await checkProxiedFetch({
FETCH_URL: requestUrl,
// Set the proxy in the environment instead of passing it to SET_GLOBAL_PROXY
http_proxy: proxyUrl,
SET_GLOBAL_PROXY_DEFAULT: '1', // Signal to call without arguments
}, {
stdout: 'Hello world',
});
shutdown();
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,21 @@
// Tests that http.setGlobalProxyFromEnv() is a no-op when no proxy is configured.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
const { proxyLogs, shutdown, httpEndpoint: { requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedFetch({
FETCH_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({}),
}, {
stdout: 'Hello world',
});
shutdown();
// Verify request did NOT go through proxy.
assert.deepStrictEqual(proxyLogs, []);
@@ -0,0 +1,38 @@
// Tests that http.setGlobalProxyFromEnv() works with fetch().
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import * as fixtures from '../common/fixtures.mjs';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
if (!common.hasCrypto) {
common.skip('Needs crypto');
}
const { proxyLogs, proxyUrl, shutdown, httpsEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpsEndpoint: true,
});
await checkProxiedFetch({
FETCH_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
https_proxy: proxyUrl,
}),
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
}, {
stdout: 'Hello world',
});
shutdown();
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,23 @@
// Tests that http.setGlobalProxyFromEnv() works with no_proxy configuration.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedFetch({
FETCH_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: proxyUrl,
no_proxy: 'localhost',
}),
}, {
stdout: 'Hello world',
});
shutdown();
// Verify request did NOT go through proxy.
assert.deepStrictEqual(proxyLogs, []);
@@ -0,0 +1,35 @@
// Tests that http.setGlobalProxyFromEnv() works with fetch() and https.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
const { proxyLogs, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedFetch({
FETCH_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: proxyUrl,
}),
CLEAR_GLOBAL_PROXY_AND_RETRY: '1',
}, {
stdout: 'Hello world\nHello world',
});
shutdown();
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,34 @@
// Tests that http.setGlobalProxyFromEnv() works with fetch() and https.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedFetch } from '../common/proxy-server.js';
const { proxyLogs, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedFetch({
FETCH_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: proxyUrl,
}),
}, {
stdout: 'Hello world',
});
shutdown();
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,33 @@
// Tests that http.setGlobalProxyFromEnv() without arguments uses process.env.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
const { proxyLogs, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
// Test that calling setGlobalProxyFromEnv() without arguments uses process.env
await checkProxiedRequest({
REQUEST_URL: requestUrl,
// Set the proxy in the environment instead of passing it to SET_GLOBAL_PROXY
http_proxy: proxyUrl,
SET_GLOBAL_PROXY_DEFAULT: '1', // Signal to call without arguments
}, {
stdout: 'Hello world',
});
shutdown();
const expectedLogs = [{
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,20 @@
// Tests that http.setGlobalProxyFromEnv() is a no-op when no proxy is configured.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
const { proxyLogs, shutdown, httpEndpoint: { requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedRequest({
REQUEST_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({}),
}, {
stdout: 'Hello world',
});
shutdown();
// Verify request did NOT go through proxy.
assert.deepStrictEqual(proxyLogs, []);
@@ -0,0 +1,33 @@
// Tests that http.setGlobalProxyFromEnv() works with fetch() and https.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
const { proxyLogs, proxyUrl, shutdown, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedRequest({
REQUEST_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: proxyUrl,
}),
CLEAR_GLOBAL_PROXY_AND_RETRY: '1',
}, {
stdout: 'Hello worldHello world',
});
shutdown();
const expectedLogs = [{
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,29 @@
// Tests that http.setGlobalProxyFromEnv() works with http.request().
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { requestUrl, serverHost } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedRequest({
REQUEST_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({ http_proxy: proxyUrl }),
}, {
stdout: 'Hello world',
});
shutdown();
const expectedLogs = [{
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,37 @@
// Tests that http.setGlobalProxyFromEnv() works with https.request().
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import * as fixtures from '../common/fixtures.mjs';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
if (!common.hasCrypto) {
common.skip('Needs crypto');
}
const { proxyLogs, httpsEndpoint: { requestUrl, serverHost }, proxyUrl, shutdown } = await startTestServers({
httpsEndpoint: true,
});
await checkProxiedRequest({
REQUEST_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
https_proxy: proxyUrl,
}),
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
}, {
stdout: 'Hello world',
});
shutdown();
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}];
assert.deepStrictEqual(proxyLogs, expectedLogs);
@@ -0,0 +1,15 @@
// Tests that http.setGlobalProxyFromEnv() validates that proxyEnv is an object.
import '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
for (const invalidInput of [42, 'string', null, [], true]) {
assert.throws(
() => http.setGlobalProxyFromEnv(invalidInput),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
},
);
}
@@ -0,0 +1,21 @@
// Tests that http.setGlobalProxyFromEnv() throws on invalid HTTP proxy URL.
import '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
assert.throws(() => {
http.setGlobalProxyFromEnv({
http_proxy: 'not a url',
});
}, {
code: 'ERR_PROXY_INVALID_CONFIG',
});
assert.throws(() => {
http.setGlobalProxyFromEnv({
https_proxy: 'not a url',
});
}, {
code: 'ERR_PROXY_INVALID_CONFIG',
});
@@ -0,0 +1,23 @@
// Tests that http.setGlobalProxyFromEnv() works with no_proxy configuration.
import '../common/index.mjs';
import assert from 'node:assert';
import { startTestServers, checkProxiedRequest } from '../common/proxy-server.js';
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { requestUrl } } = await startTestServers({
httpEndpoint: true,
});
await checkProxiedRequest({
REQUEST_URL: requestUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: proxyUrl,
no_proxy: 'localhost',
}),
}, {
stdout: 'Hello world',
});
shutdown();
// Verify request did NOT go through proxy.
assert.deepStrictEqual(proxyLogs, []);
@@ -0,0 +1,44 @@
// Tests that http.setGlobalProxyFromEnv() overrides configuration from NODE_USE_ENV_PROXY.
import '../common/index.mjs';
import assert from 'node:assert';
import { once } from 'node:events';
import { startTestServers, checkProxiedFetch, createProxyServer } from '../common/proxy-server.js';
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
const { proxy: proxy2, logs: proxyLogs2 } = createProxyServer();
proxy2.listen(0);
await once(proxy2, 'listening');
await checkProxiedFetch({
FETCH_URL: requestUrl,
NODE_USE_ENV_PROXY: '1',
http_proxy: proxyUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: `http://localhost:${proxy2.address().port}`,
}),
}, {
stdout: 'Hello world',
});
shutdown();
proxy2.close();
// Verify request did NOT go through original proxy, but the overriden one.
assert.deepStrictEqual(proxyLogs, []);
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false, but what we
// need is for it to be automatically false for HTTP requests to be
// consistent with curl.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs2, expectedLogs);
@@ -0,0 +1,40 @@
// Tests that http.setGlobalProxyFromEnv() overrides configuration from NODE_USE_ENV_PROXY.
import '../common/index.mjs';
import assert from 'node:assert';
import { once } from 'node:events';
import { startTestServers, checkProxiedRequest, createProxyServer } from '../common/proxy-server.js';
const { proxyLogs, shutdown, proxyUrl, httpEndpoint: { serverHost, requestUrl } } = await startTestServers({
httpEndpoint: true,
});
const { proxy: proxy2, logs: proxyLogs2 } = createProxyServer();
proxy2.listen(0);
await once(proxy2, 'listening');
await checkProxiedRequest({
REQUEST_URL: requestUrl,
NODE_USE_ENV_PROXY: '1',
http_proxy: proxyUrl,
SET_GLOBAL_PROXY: JSON.stringify({
http_proxy: `http://localhost:${proxy2.address().port}`,
}),
}, {
stdout: 'Hello world',
});
shutdown();
proxy2.close();
// Verify request did NOT go through original proxy, but the overriden one.
assert.deepStrictEqual(proxyLogs, []);
const expectedLogs = [{
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'host': serverHost,
'proxy-connection': 'keep-alive',
},
}];
assert.deepStrictEqual(proxyLogs2, expectedLogs);
+69 -5
View File
@@ -3,6 +3,8 @@
const net = require('net');
const http = require('http');
const assert = require('assert');
const { once } = require('events');
const fixtures = require('./fixtures');
function logRequest(logs, req) {
logs.push({
@@ -14,7 +16,7 @@ function logRequest(logs, req) {
// This creates a minimal proxy server that logs the requests it gets
// to an array before performing proxying.
exports.createProxyServer = function(options = {}) {
function createProxyServer(options = {}) {
const logs = [];
let proxy;
@@ -107,7 +109,8 @@ exports.createProxyServer = function(options = {}) {
});
return { proxy, logs };
};
}
exports.createProxyServer = createProxyServer;
function spawnPromisified(...args) {
const { spawn } = require('child_process');
@@ -148,12 +151,13 @@ function spawnPromisified(...args) {
});
}
exports.checkProxiedFetch = async function(envExtension, expectation, cliArgsExtension = []) {
const fixtures = require('./fixtures');
async function checkProxied(type, envExtension, expectation, cliArgsExtension = []) {
const script = type === 'fetch' ? fixtures.path('fetch-and-log.mjs') : fixtures.path('request-and-log.js');
const { code, signal, stdout, stderr } = await spawnPromisified(
process.execPath,
[...cliArgsExtension, fixtures.path('fetch-and-log.mjs')], {
[...cliArgsExtension, script], {
env: {
NO_LOG_REQUEST: '1',
...process.env,
...envExtension,
},
@@ -172,6 +176,14 @@ exports.checkProxiedFetch = async function(envExtension, expectation, cliArgsExt
});
};
exports.checkProxiedFetch = async function(...args) {
return checkProxied('fetch', ...args);
};
exports.checkProxiedRequest = async function(...args) {
return checkProxied('request', ...args);
};
exports.runProxiedRequest = async function(envExtension, cliArgsExtension = []) {
const fixtures = require('./fixtures');
return spawnPromisified(
@@ -195,3 +207,55 @@ exports.runProxiedPOST = async function(envExtension) {
},
});
};
exports.startTestServers = async function(options = {}) {
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
let httpServer, httpsServer, httpEndpoint, httpsEndpoint;
if (options.httpsEndpoint) {
httpsServer = require('https').createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, (req, res) => {
res.end('Hello world');
});
httpsServer.listen(0);
await once(httpsServer, 'listening');
const { port } = httpsServer.address();
httpsEndpoint = {
serverHost: `localhost:${port}`,
requestUrl: `https://localhost:${port}/test`,
};
}
if (options.httpEndpoint) {
httpServer = http.createServer((req, res) => {
res.end('Hello world');
});
httpServer.listen(0);
await once(httpServer, 'listening');
const { port } = httpServer.address();
httpEndpoint = {
serverHost: `localhost:${port}`,
requestUrl: `http://localhost:${port}/test`,
};
}
return {
proxyLogs: logs,
shutdown() {
if (httpServer) {
httpServer.close();
}
if (httpsServer) {
httpsServer.close();
}
proxy.close();
},
proxyUrl: `http://localhost:${proxy.address().port}`,
httpEndpoint,
httpsEndpoint,
};
};
+17
View File
@@ -1,3 +1,20 @@
import http from 'node:http';
let restore;
if (process.env.SET_GLOBAL_PROXY) {
const config = JSON.parse(process.env.SET_GLOBAL_PROXY);
restore = http.setGlobalProxyFromEnv(config);
} else if (process.env.SET_GLOBAL_PROXY_DEFAULT) {
restore = http.setGlobalProxyFromEnv();
}
const response = await fetch(process.env.FETCH_URL);
const body = await response.text();
console.log(body);
if (process.env.CLEAR_GLOBAL_PROXY_AND_RETRY) {
restore();
const response = await fetch(process.env.FETCH_URL);
const body = await response.text();
console.log(body);
}
+41 -18
View File
@@ -1,3 +1,11 @@
let restore;
if (process.env.SET_GLOBAL_PROXY) {
const config = JSON.parse(process.env.SET_GLOBAL_PROXY);
restore = require('http').setGlobalProxyFromEnv(config);
} else if (process.env.SET_GLOBAL_PROXY_DEFAULT) {
restore = require('http').setGlobalProxyFromEnv();
}
const url = process.env.REQUEST_URL;
let lib;
@@ -32,24 +40,39 @@ if (process.env.RESOLVE_TO_LOCALHOST) {
};
}
const req = request(url, {
timeout,
agent,
lookup,
}, (res) => {
// Log the status code
console.log(`Status Code: ${res.statusCode}`);
console.log('Headers:', res.headers);
res.pipe(process.stdout);
});
function startRequest(callback) {
const req = request(url, {
timeout,
agent,
lookup,
}, (res) => {
// Log the status code
if (!process.env.NO_LOG_REQUEST) {
console.log(`Status Code: ${res.statusCode}`);
console.log('Headers:', res.headers);
}
res.pipe(process.stdout);
req.on('error', (e) => {
console.error('Request Error', e);
});
if (callback) {
res.on('end', callback);
}
});
req.on('timeout', () => {
console.error('Request timed out');
req.destroy();
});
req.on('error', (e) => {
console.error('Request Error', e);
});
req.end();
req.on('timeout', () => {
console.error('Request timed out');
req.destroy();
});
req.end();
}
startRequest(() => {
if (process.env.CLEAR_GLOBAL_PROXY_AND_RETRY) {
restore();
startRequest();
}
});