Skip to content

Commit 20ce9c4

Browse files
LaplaceYounglaplace youngjasonsaayman
authored
fix(http): defer env proxy handling to Node (#10942)
Co-authored-by: laplace young <yangqk12@whu.edu.cn> Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
1 parent e64bcf9 commit 20ce9c4

4 files changed

Lines changed: 244 additions & 8 deletions

File tree

‎PRE_RELEASE_CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## Bug Fixes
66

7+
- **HTTP Adapter - native env proxy:** Avoid double-applying environment proxy handling when Node.js native HTTP proxy support is active for the selected agent. Axios still resolves env proxies itself when the selected agent is not using Node's `proxyEnv` support. (**#10942**, closes **#7299**)
78
- **HTTP Adapter - socketPath:** Path-only request URLs (e.g. `'/foo'`) now work again with `config.socketPath`, fixing the `TypeError [ERR_INVALID_URL]` regression introduced in 1.7.4 when `new URL()` was added to the dispatch path. A synthetic `http://localhost` base is supplied only when an own `socketPath` is set, so absolute URLs, non-socket requests, and prototype-polluted `socketPath` values are unaffected. (**#6611**)
89

910
## Release Tracking

‎PRE_RELEASE_DOCS.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ Do not store raw diffs or line-number-only instructions here; prefer stable sect
1919
- **Notes:** Constraints, release-only wording, translation follow-up, etc.
2020

2121
## Unreleased
22+
23+
### Node native env proxy interaction
24+
25+
- **Change:** Document how the Node.js HTTP adapter interacts with Node native environment proxy handling.
26+
- **Source:** `PRE_RELEASE_CHANGELOG.md` Bug Fixes, #10942, closes #7299.
27+
- **Status:** Pending.
28+
- **Docs targets:** `README.md` proxy/request config sections; `docs/pages/advanced/request-config.md`; translated request-config docs after English docs are finalized.
29+
- **Required content:** Explain that axios normally resolves `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` itself for the Node.js HTTP adapter unless `config.proxy` is `false`. On Node.js versions with native HTTP proxy support, axios defers environment proxy handling to Node when the selected HTTP/HTTPS agent has `proxyEnv` enabled, including processes started with `NODE_USE_ENV_PROXY=1`, `--use-env-proxy`, or `NODE_OPTIONS=--use-env-proxy`. Custom agents without `proxyEnv` continue to use axios env proxy resolution. Explicit `config.proxy` remains handled by axios.
30+
- **Examples:** None required.
31+
- **Notes:** Keep the wording Node.js-only. Mention that this avoids double proxy rewriting while preserving existing custom-agent behavior.

‎lib/adapters/http.js‎

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,53 @@ const kAxiosInstalledTunnel = Symbol('axios.http.installedTunnel');
8989
// so unbounded growth is not a concern in practice.
9090
const tunnelingAgentCache = new Map();
9191
const tunnelingAgentCacheUser = new WeakMap();
92+
// Minimum minor versions where Node's HTTP Agent supports native proxyEnv
93+
// handling. Checking the selected agent below also covers startup modes such
94+
// as NODE_OPTIONS=--use-env-proxy and --no-use-env-proxy precedence.
95+
const NODE_NATIVE_ENV_PROXY_SUPPORT = {
96+
22: 21,
97+
24: 5,
98+
};
99+
100+
function isNodeNativeEnvProxySupported(nodeVersion = process.versions && process.versions.node) {
101+
if (!nodeVersion) {
102+
return false;
103+
}
104+
105+
const [major, minor] = nodeVersion.split('.').map((part) => Number(part));
106+
107+
if (!Number.isInteger(major) || !Number.isInteger(minor)) {
108+
return false;
109+
}
110+
111+
if (major > 24) {
112+
return true;
113+
}
114+
115+
return (
116+
NODE_NATIVE_ENV_PROXY_SUPPORT[major] != null && minor >= NODE_NATIVE_ENV_PROXY_SUPPORT[major]
117+
);
118+
}
119+
120+
function isNodeEnvProxyEnabled(agent, nodeVersion = process.versions && process.versions.node) {
121+
if (!isNodeNativeEnvProxySupported(nodeVersion)) {
122+
return false;
123+
}
124+
125+
const agentOptions = agent && agent.options;
126+
127+
return Boolean(
128+
agentOptions &&
129+
utils.hasOwnProp(agentOptions, 'proxyEnv') &&
130+
agentOptions.proxyEnv != null
131+
);
132+
}
133+
134+
function getProxyEnvAgent(options, configHttpAgent, configHttpsAgent) {
135+
return isHttps.test(options.protocol)
136+
? (configHttpsAgent || https.globalAgent)
137+
: (configHttpAgent || http.globalAgent);
138+
}
92139

93140
function getTunnelingAgent(agentOptions, userHttpsAgent) {
94141
const key =
@@ -210,9 +257,10 @@ function isSameOriginRedirect(redirectOptions, requestDetails) {
210257
*
211258
* @returns {http.ClientRequestArgs}
212259
*/
213-
function setProxy(options, configProxy, location, isRedirect, configHttpsAgent) {
260+
function setProxy(options, configProxy, location, isRedirect, configHttpsAgent, configHttpAgent) {
214261
let proxy = configProxy;
215-
if (!proxy && proxy !== false) {
262+
const proxyEnvAgent = getProxyEnvAgent(options, configHttpAgent, configHttpsAgent);
263+
if (!proxy && proxy !== false && !isNodeEnvProxyEnabled(proxyEnvAgent)) {
216264
const proxyUrl = getProxyForUrl(location);
217265
if (proxyUrl) {
218266
if (!shouldBypassProxy(location)) {
@@ -363,7 +411,14 @@ function setProxy(options, configProxy, location, isRedirect, configHttpsAgent)
363411
options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {
364412
// Configure proxy for redirected request, passing the original config proxy to apply
365413
// the exact same logic as if the redirected request was performed by axios directly.
366-
setProxy(redirectOptions, configProxy, redirectOptions.href, true, configHttpsAgent);
414+
setProxy(
415+
redirectOptions,
416+
configProxy,
417+
redirectOptions.href,
418+
true,
419+
configHttpsAgent,
420+
configHttpAgent
421+
);
367422
};
368423
}
369424

@@ -475,11 +530,12 @@ export default isHttpAdapterSupported &&
475530
let httpVersion = own('httpVersion');
476531
if (httpVersion === undefined) httpVersion = 1;
477532
let http2Options = own('http2Options');
533+
const httpAgent = own('httpAgent');
534+
const httpsAgent = own('httpsAgent');
535+
const configProxy = own('proxy');
478536
const responseType = own('responseType');
479537
const responseEncoding = own('responseEncoding');
480538
const socketPath = own('socketPath');
481-
const httpAgent = own('httpAgent');
482-
const httpsAgent = own('httpsAgent');
483539
const method = own('method').toUpperCase();
484540
const maxRedirects = own('maxRedirects');
485541
const maxBodyLength = own('maxBodyLength');
@@ -887,10 +943,11 @@ export default isHttpAdapterSupported &&
887943
options.port = parsed.port;
888944
setProxy(
889945
options,
890-
own('proxy'),
946+
configProxy,
891947
protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path,
892948
false,
893-
httpsAgent
949+
httpsAgent,
950+
httpAgent
894951
);
895952
}
896953
let transport;
@@ -1353,4 +1410,5 @@ export default isHttpAdapterSupported &&
13531410
};
13541411

13551412
export const __setProxy = setProxy;
1413+
export const __isNodeEnvProxyEnabled = isNodeEnvProxyEnabled;
13561414
export const __isSameOriginRedirect = isSameOriginRedirect;

‎tests/unit/adapters/http.test.js‎

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
} from '../../setup/server.js';
1111
import axios from '../../../index.js';
1212
import AxiosError from '../../../lib/core/AxiosError.js';
13-
import httpAdapter, { __isSameOriginRedirect, __setProxy } from '../../../lib/adapters/http.js';
13+
import httpAdapter, {
14+
__isNodeEnvProxyEnabled,
15+
__isSameOriginRedirect,
16+
__setProxy,
17+
} from '../../../lib/adapters/http.js';
1418
import HttpsProxyAgent from 'https-proxy-agent';
1519
import http from 'http';
1620
import https from 'https';
@@ -2565,6 +2569,169 @@ describe('supports http with nodejs', () => {
25652569
}
25662570
});
25672571

2572+
it('should detect Node native env proxy support from the selected agent', () => {
2573+
const nativeProxyAgent = { options: { proxyEnv: { HTTP_PROXY: 'http://proxy.local:9000' } } };
2574+
const plainAgent = { options: {} };
2575+
2576+
assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '22.20.0'), false);
2577+
assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '22.21.0'), true);
2578+
assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '24.4.0'), false);
2579+
assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '24.5.0'), true);
2580+
assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '25.0.0'), true);
2581+
assert.strictEqual(__isNodeEnvProxyEnabled(plainAgent, '24.5.0'), false);
2582+
assert.strictEqual(__isNodeEnvProxyEnabled(undefined, '24.5.0'), false);
2583+
});
2584+
2585+
it('should leave env proxy handling to supported Node versions when the selected agent uses proxyEnv', () => {
2586+
const originalHttpProxy = process.env.http_proxy;
2587+
const originalHTTPProxy = process.env.HTTP_PROXY;
2588+
const originalNoProxy = process.env.no_proxy;
2589+
const originalNOProxy = process.env.NO_PROXY;
2590+
const originalNodeUseEnvProxy = process.env.NODE_USE_ENV_PROXY;
2591+
2592+
process.env.NODE_USE_ENV_PROXY = '1';
2593+
process.env.http_proxy = 'http://proxy.local:9000/';
2594+
process.env.HTTP_PROXY = 'http://proxy.local:9000/';
2595+
process.env.no_proxy = '';
2596+
process.env.NO_PROXY = '';
2597+
2598+
try {
2599+
const options = {
2600+
headers: {},
2601+
beforeRedirects: {},
2602+
hostname: 'target.example',
2603+
host: 'target.example',
2604+
port: '4000',
2605+
protocol: 'http:',
2606+
path: '/resource',
2607+
};
2608+
const nativeProxyAgent = { options: { proxyEnv: process.env } };
2609+
2610+
__setProxy(
2611+
options,
2612+
undefined,
2613+
'http://target.example:4000/resource',
2614+
false,
2615+
undefined,
2616+
nativeProxyAgent
2617+
);
2618+
2619+
if (__isNodeEnvProxyEnabled(nativeProxyAgent, process.versions.node)) {
2620+
assert.strictEqual(options.hostname, 'target.example');
2621+
assert.strictEqual(options.port, '4000');
2622+
assert.strictEqual(options.path, '/resource');
2623+
assert.strictEqual(options.headers.host, undefined);
2624+
} else {
2625+
assert.strictEqual(options.hostname, 'proxy.local');
2626+
assert.strictEqual(options.port, '9000');
2627+
assert.strictEqual(options.path, 'http://target.example:4000/resource');
2628+
}
2629+
2630+
assert.strictEqual(typeof options.beforeRedirects.proxy, 'function');
2631+
} finally {
2632+
if (originalHttpProxy === undefined) {
2633+
delete process.env.http_proxy;
2634+
} else {
2635+
process.env.http_proxy = originalHttpProxy;
2636+
}
2637+
2638+
if (originalHTTPProxy === undefined) {
2639+
delete process.env.HTTP_PROXY;
2640+
} else {
2641+
process.env.HTTP_PROXY = originalHTTPProxy;
2642+
}
2643+
2644+
if (originalNoProxy === undefined) {
2645+
delete process.env.no_proxy;
2646+
} else {
2647+
process.env.no_proxy = originalNoProxy;
2648+
}
2649+
2650+
if (originalNOProxy === undefined) {
2651+
delete process.env.NO_PROXY;
2652+
} else {
2653+
process.env.NO_PROXY = originalNOProxy;
2654+
}
2655+
2656+
if (originalNodeUseEnvProxy === undefined) {
2657+
delete process.env.NODE_USE_ENV_PROXY;
2658+
} else {
2659+
process.env.NODE_USE_ENV_PROXY = originalNodeUseEnvProxy;
2660+
}
2661+
}
2662+
});
2663+
2664+
it('should keep axios env proxy handling when the selected agent has no proxyEnv', () => {
2665+
const originalHttpProxy = process.env.http_proxy;
2666+
const originalHTTPProxy = process.env.HTTP_PROXY;
2667+
const originalNoProxy = process.env.no_proxy;
2668+
const originalNOProxy = process.env.NO_PROXY;
2669+
const originalNodeUseEnvProxy = process.env.NODE_USE_ENV_PROXY;
2670+
2671+
process.env.NODE_USE_ENV_PROXY = '1';
2672+
process.env.http_proxy = 'http://proxy.local:9000/';
2673+
process.env.HTTP_PROXY = 'http://proxy.local:9000/';
2674+
process.env.no_proxy = '';
2675+
process.env.NO_PROXY = '';
2676+
2677+
try {
2678+
const options = {
2679+
headers: {},
2680+
beforeRedirects: {},
2681+
hostname: 'target.example',
2682+
host: 'target.example',
2683+
port: '4000',
2684+
protocol: 'http:',
2685+
path: '/resource',
2686+
};
2687+
const plainAgent = { options: {} };
2688+
2689+
__setProxy(
2690+
options,
2691+
undefined,
2692+
'http://target.example:4000/resource',
2693+
false,
2694+
undefined,
2695+
plainAgent
2696+
);
2697+
2698+
assert.strictEqual(options.hostname, 'proxy.local');
2699+
assert.strictEqual(options.port, '9000');
2700+
assert.strictEqual(options.path, 'http://target.example:4000/resource');
2701+
assert.strictEqual(typeof options.beforeRedirects.proxy, 'function');
2702+
} finally {
2703+
if (originalHttpProxy === undefined) {
2704+
delete process.env.http_proxy;
2705+
} else {
2706+
process.env.http_proxy = originalHttpProxy;
2707+
}
2708+
2709+
if (originalHTTPProxy === undefined) {
2710+
delete process.env.HTTP_PROXY;
2711+
} else {
2712+
process.env.HTTP_PROXY = originalHTTPProxy;
2713+
}
2714+
2715+
if (originalNoProxy === undefined) {
2716+
delete process.env.no_proxy;
2717+
} else {
2718+
process.env.no_proxy = originalNoProxy;
2719+
}
2720+
2721+
if (originalNOProxy === undefined) {
2722+
delete process.env.NO_PROXY;
2723+
} else {
2724+
process.env.NO_PROXY = originalNOProxy;
2725+
}
2726+
2727+
if (originalNodeUseEnvProxy === undefined) {
2728+
delete process.env.NODE_USE_ENV_PROXY;
2729+
} else {
2730+
process.env.NODE_USE_ENV_PROXY = originalNodeUseEnvProxy;
2731+
}
2732+
}
2733+
});
2734+
25682735
it('should support HTTPS proxy set via env var', async () => {
25692736
const originalHttpsProxy = process.env.https_proxy;
25702737
const originalHTTPSProxy = process.env.HTTPS_PROXY;

0 commit comments

Comments
 (0)