refactor(cli-common): deprecate bootstrapEnvProxyAgents() in favor of Node.js built-in proxy support

Node.js 22.21.0+ and 24.5.0+ support proxy configuration natively
via NODE_USE_ENV_PROXY=1 and --use-env-proxy, making the legacy
global-agent and undici proxy workarounds unnecessary.

Rather than removing the function immediately, deprecate it with
context-aware runtime warnings that guide users based on their
current configuration:

- Users with GLOBAL_AGENT_* vars are told to switch to standard
  HTTP_PROXY/HTTPS_PROXY and set NODE_USE_ENV_PROXY=1.
- Users with HTTP_PROXY/HTTPS_PROXY but no NODE_USE_ENV_PROXY are
  told to set it.
- Users who have already opted in to Node.js built-in proxy see no
  warning, and the legacy bootstrap is skipped entirely.

See #33444

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops
2026-04-10 16:29:04 +02:00
parent f0d82ac4b0
commit 46ff47034c
4 changed files with 187 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-common': patch
---
Deprecated `bootstrapEnvProxyAgents()` in favor of Node.js built-in proxy support. Set `NODE_USE_ENV_PROXY=1` alongside your `HTTP_PROXY`/`HTTPS_PROXY` environment variables instead. See the [corporate proxy guide](https://backstage.io/docs/tutorials/corporate-proxy/) for details. This function will be removed in a future release.
+1 -1
View File
@@ -10,7 +10,7 @@ import { SpawnOptions } from 'node:child_process';
// @public
export const BACKSTAGE_JSON = 'backstage.json';
// @public
// @public @deprecated
export function bootstrapEnvProxyAgents(): void;
// @public
@@ -26,14 +26,17 @@ jest.mock('undici', () => ({
describe('bootstrapEnvProxyAgents', () => {
const originalEnv = process.env;
const originalExecArgv = process.execArgv;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
process.execArgv = [...originalExecArgv];
});
afterEach(() => {
process.env = originalEnv;
process.execArgv = originalExecArgv;
jest.clearAllMocks();
});
@@ -101,8 +104,153 @@ describe('bootstrapEnvProxyAgents', () => {
const { bootstrap } =
require('global-agent') as typeof import('global-agent');
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(bootstrap).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('CUSTOM_AGENT_*'),
expect.objectContaining({
type: 'DeprecationWarning',
code: 'BACKSTAGE_CLI_GLOBAL_AGENT_PROXY',
}),
);
spy.mockRestore();
});
it('should skip undici dispatcher but still bootstrap global-agent when NODE_USE_ENV_PROXY is set', () => {
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com';
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.env.NODE_USE_ENV_PROXY = '1';
const { bootstrap } =
require('global-agent') as typeof import('global-agent');
const { setGlobalDispatcher } =
require('undici') as typeof import('undici');
bootstrapEnvProxyAgents();
expect(bootstrap).toHaveBeenCalledTimes(1);
expect(setGlobalDispatcher).not.toHaveBeenCalled();
});
it('should skip undici dispatcher when --use-env-proxy is in execArgv', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.execArgv = ['--use-env-proxy'];
const { setGlobalDispatcher } =
require('undici') as typeof import('undici');
bootstrapEnvProxyAgents();
expect(setGlobalDispatcher).not.toHaveBeenCalled();
});
it('should skip undici dispatcher when --use-env-proxy is in NODE_OPTIONS', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.env.NODE_OPTIONS = '--use-env-proxy';
const { setGlobalDispatcher } =
require('undici') as typeof import('undici');
bootstrapEnvProxyAgents();
expect(setGlobalDispatcher).not.toHaveBeenCalled();
});
it('should emit a deprecation warning when standard proxy vars are set without NODE_USE_ENV_PROXY', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('NODE_USE_ENV_PROXY=1'),
expect.objectContaining({
type: 'DeprecationWarning',
code: 'BACKSTAGE_CLI_PROXY_BOOTSTRAP',
}),
);
spy.mockRestore();
});
it('should not emit a deprecation warning when standard proxy vars are set with NODE_USE_ENV_PROXY', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.env.NODE_USE_ENV_PROXY = '1';
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
it('should not emit a deprecation warning when --use-env-proxy is in execArgv', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.execArgv = ['--use-env-proxy'];
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
it('should not emit a deprecation warning when --use-env-proxy is in NODE_OPTIONS', () => {
process.env.HTTP_PROXY = 'http://proxy.example.com';
process.env.NODE_OPTIONS = '--use-env-proxy';
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
it('should emit a deprecation warning when GLOBAL_AGENT proxy vars are set', () => {
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com';
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('GLOBAL_AGENT_*'),
expect.objectContaining({
type: 'DeprecationWarning',
code: 'BACKSTAGE_CLI_GLOBAL_AGENT_PROXY',
}),
);
spy.mockRestore();
});
it('should emit a deprecation warning for GLOBAL_AGENT proxy vars even with NODE_USE_ENV_PROXY', () => {
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com';
process.env.NODE_USE_ENV_PROXY = '1';
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('GLOBAL_AGENT_*'),
expect.objectContaining({
type: 'DeprecationWarning',
code: 'BACKSTAGE_CLI_GLOBAL_AGENT_PROXY',
}),
);
spy.mockRestore();
});
it('should not emit a deprecation warning when no proxy vars are set', () => {
const spy = jest.spyOn(process, 'emitWarning');
bootstrapEnvProxyAgents();
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
});
+33 -5
View File
@@ -27,21 +27,49 @@
* Make sure to call this function before any other imports.
*
* @public
* @deprecated Use Node.js built-in proxy support by setting `NODE_USE_ENV_PROXY=1`
* alongside your `HTTP_PROXY` / `HTTPS_PROXY` environment variables instead.
* This function will be removed in a future release.
* See {@link https://backstage.io/docs/tutorials/corporate-proxy/ | the corporate proxy guide} for details.
*/
export function bootstrapEnvProxyAgents() {
// see https://www.npmjs.com/package/global-agent
const globalAgentNamespace =
process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_';
if (
const hasGlobalAgentProxy =
process.env[`${globalAgentNamespace}HTTP_PROXY`] ||
process.env[`${globalAgentNamespace}HTTPS_PROXY`]
) {
process.env[`${globalAgentNamespace}HTTPS_PROXY`];
const hasStandardProxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
// Mimics the internal getOptionValue('--use-env-proxy') check in Node.js, which
// normalizes the --use-env-proxy CLI flag and NODE_USE_ENV_PROXY=1 env var.
// https://github.com/nodejs/node/blob/v22.x/src/node_options.cc
const hasNodeEnvProxy =
process.env.NODE_USE_ENV_PROXY === '1' ||
process.execArgv.includes('--use-env-proxy') ||
(process.env.NODE_OPTIONS?.split(/\s+/) ?? []).includes('--use-env-proxy');
// Node.js never reads GLOBAL_AGENT_* vars, so global-agent must always
// handle those to preserve behavior during the deprecation period.
if (hasGlobalAgentProxy) {
process.emitWarning(
`Configuration of proxy agents through ${globalAgentNamespace}* environment variables is deprecated and will be removed in a future release. Switch to the standard HTTP_PROXY/HTTPS_PROXY environment variables and set NODE_USE_ENV_PROXY=1 instead. See https://backstage.io/docs/tutorials/corporate-proxy/ for details.`,
{ type: 'DeprecationWarning', code: 'BACKSTAGE_CLI_GLOBAL_AGENT_PROXY' },
);
const globalAgent =
require('global-agent') as typeof import('global-agent');
globalAgent.bootstrap();
}
if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) {
// Skip undici dispatcher setup when Node.js built-in proxy support is active,
// as it already configures the global dispatcher during startup.
if (hasStandardProxy && !hasNodeEnvProxy) {
process.emitWarning(
'bootstrapEnvProxyAgents() is deprecated and will be removed in a future release. Set NODE_USE_ENV_PROXY=1 to use Node.js built-in proxy support instead. See https://backstage.io/docs/tutorials/corporate-proxy/ for details.',
{ type: 'DeprecationWarning', code: 'BACKSTAGE_CLI_PROXY_BOOTSTRAP' },
);
const { setGlobalDispatcher, EnvHttpProxyAgent } =
require('undici') as typeof import('undici');
setGlobalDispatcher(new EnvHttpProxyAgent());