diff --git a/.changeset/deprecate-proxy-bootstrap.md b/.changeset/deprecate-proxy-bootstrap.md new file mode 100644 index 0000000000..178abdd39f --- /dev/null +++ b/.changeset/deprecate-proxy-bootstrap.md @@ -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. diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index ba649cacc9..b35985dd67 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -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 diff --git a/packages/cli-common/src/proxyBootstrap.test.ts b/packages/cli-common/src/proxyBootstrap.test.ts index cefa1bd36e..a505ce477c 100644 --- a/packages/cli-common/src/proxyBootstrap.test.ts +++ b/packages/cli-common/src/proxyBootstrap.test.ts @@ -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(); }); }); diff --git a/packages/cli-common/src/proxyBootstrap.ts b/packages/cli-common/src/proxyBootstrap.ts index b7375b6e55..23e40821f9 100644 --- a/packages/cli-common/src/proxyBootstrap.ts +++ b/packages/cli-common/src/proxyBootstrap.ts @@ -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());