From e00bf413008fe2adcc5ba36c6e578071d96e49bd Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Mon, 26 Oct 2026 14:37:43 +0100 Subject: [PATCH] fix(config.reader): fall back to legacy proxy for pinned pnpm (#17363) createPackageManagerNetworkConfig now reads `https-proxy`, `http-proxy`, and `proxy` as unset unless they are a non-empty string. `false`, `null`, and an empty value fall through to the next setting and then the environment, matching resolveProxySettings for installs and pnpm v12. An explicit `proxy=false` remains the way to turn the proxy off, and it now also stops the download's `httpProxy` from falling back to `HTTP_PROXY`, as installs do. The engine identity check of the pinned pnpm now takes its network settings from getPackageManagerBootstrapConfig, like the store controller that downloads it, instead of the project's proxy, TLS, and auth settings. Closes pnpm/tasks#193 --------- Co-authored-by: Zoltan Kochan Co-authored-by: Claude Opus 5.5 --- .../proxy-fallback-for-bootstrap-downloads.md | 6 ++ .../config/reader/src/getConfig/registries.ts | 30 ++++-- pnpm11/config/reader/test/index.ts | 96 +++++++++++++++++++ pnpm11/pnpm/src/switchCliVersion.test.ts | 22 +++++ pnpm11/pnpm/src/switchCliVersion.ts | 15 +-- 5 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 .changeset/proxy-fallback-for-bootstrap-downloads.md diff --git a/.changeset/proxy-fallback-for-bootstrap-downloads.md b/.changeset/proxy-fallback-for-bootstrap-downloads.md new file mode 100644 index 0000000000..acbcf4b93e --- /dev/null +++ b/.changeset/proxy-fallback-for-bootstrap-downloads.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config.reader": patch +"pnpm": patch +--- + +When `https-proxy` is `false`, pnpm now downloads the pnpm version pinned by `packageManager` through the legacy `proxy` setting, as installs already do. Setting `proxy` to `false` now also stops that download from using the `HTTP_PROXY` environment variable. The signature check of the pinned pnpm now uses the same proxy and TLS settings as its download [pnpm/tasks#193](https://github.com/pnpm/tasks/issues/193). diff --git a/pnpm11/config/reader/src/getConfig/registries.ts b/pnpm11/config/reader/src/getConfig/registries.ts index 74d2726afa..e4b0271e0a 100644 --- a/pnpm11/config/reader/src/getConfig/registries.ts +++ b/pnpm11/config/reader/src/getConfig/registries.ts @@ -189,13 +189,14 @@ function createPackageManagerNetworkConfig ( // trusted `.npmrc` files, as they do for the project's requests. The // command line's `.npmrc`-spelled tokens sit inside `trustedConfig` too. const cliNetworkSettings = configFromCliOpts as BootstrapNetworkSettings - const httpsProxy = getProxyValue( - trustedConfig['https-proxy'] ?? trustedConfig.proxy, - getEnvValue(env, 'https_proxy') - ) + const httpsProxySetting = getProxyUrl(trustedConfig['https-proxy']) ?? + (trustedConfig.proxy === false ? false : getProxyUrl(trustedConfig.proxy)) + const httpsProxy = getProxyValue(httpsProxySetting, getEnvValue(env, 'https_proxy')) const httpProxy = getProxyValue( - trustedConfig['http-proxy'], - httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy') + getProxyUrl(trustedConfig['http-proxy']), + httpsProxySetting === false + ? undefined + : httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy') ) return { ca: trustedConfig.ca as string | string[] | undefined, @@ -214,10 +215,19 @@ function getEnvValue (env: Record, key: string): str return env[key] ?? env[key.toUpperCase()] ?? env[key.toLowerCase()] } -function getProxyValue (value: unknown, fallback: string | undefined): string | undefined { - if (value === false || value === null) return undefined - if (typeof value === 'string' && value.length > 0) return value - return fallback +/** + * A proxy setting of `false`, `null`, or an empty string reads as unset, so + * the next setting and then the environment apply, as they do for installs. + * Only `proxy=false` turns the proxy off, including the environment fallback + * for `http-proxy`. + */ +function getProxyUrl (value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +function getProxyValue (value: string | false | undefined, fallback: string | undefined): string | undefined { + if (value === false) return undefined + return value ?? fallback } /** diff --git a/pnpm11/config/reader/test/index.ts b/pnpm11/config/reader/test/index.ts index af664c7763..e9e665e8f7 100644 --- a/pnpm11/config/reader/test/index.ts +++ b/pnpm11/config/reader/test/index.ts @@ -6065,6 +6065,102 @@ test('proxy settings are still read from .npmrc', async () => { expect(config.noProxy).toBe('internal.example.com') }) +test('package manager downloads use the legacy proxy when https-proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=http://legacy-proxy.example.com:8080', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080') +}) + +test('package manager downloads use the legacy proxy when https-proxy is empty', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=\nproxy=http://legacy-proxy.example.com:8080', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080') +}) + +test('package manager downloads ignore the environment proxy when proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=false', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: { + HTTP_PROXY: 'http://env-proxy.example.com:8080', + HTTPS_PROXY: 'http://env-proxy.example.com:8080', + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBeUndefined() + expect(config.packageManagerNetworkConfig?.httpProxy).toBeUndefined() +}) + +test('package manager downloads use the environment proxy when proxy is null', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'proxy=null', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: { + HTTPS_PROXY: 'http://env-proxy.example.com:8080', + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://env-proxy.example.com:8080') +}) + +test('package manager downloads use the https proxy when http-proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=http://https-proxy.example.com:8080\nhttp-proxy=false', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpProxy).toBe('http://https-proxy.example.com:8080') +}) + test('an empty legacy proxy setting in .npmrc falls through to the environment', async () => { prepareEmpty() diff --git a/pnpm11/pnpm/src/switchCliVersion.test.ts b/pnpm11/pnpm/src/switchCliVersion.test.ts index b4a8c0ae51..1c42692d88 100644 --- a/pnpm11/pnpm/src/switchCliVersion.test.ts +++ b/pnpm11/pnpm/src/switchCliVersion.test.ts @@ -698,6 +698,28 @@ test('ends with the exit status of the pnpm it switched to', async () => { } }) +test('switchCliVersion verifies the pinned pnpm through the trusted proxy settings', async () => { + const config = { + httpsProxy: 'http://project-proxy.example.com:8080', + packageManagerNetworkConfig: { configByUri: {} }, + rawConfig: {}, + } as unknown as Config + const context = { + rootProjectManifestDir: '/project', + wantedPackageManager: { name: 'pnpm', version: '11.13.1', fromDevEngines: true, onFail: 'download' }, + } as unknown as ConfigContext + readEnvLockfile.mockResolvedValue(envLockfileFor('11.13.1')) + + const exit = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never) + try { + await switchCliVersion(config, context) + } finally { + exit.mockRestore() + } + + expect(installPnpmToStore).toHaveBeenCalledWith('11.13.1', expect.objectContaining({ httpsProxy: undefined })) +}) + test('reports a pnpm that cannot be started as a failed version switch', async () => { const config = { rawConfig: {} } as unknown as Config const context = { diff --git a/pnpm11/pnpm/src/switchCliVersion.ts b/pnpm11/pnpm/src/switchCliVersion.ts index a0cf5ca04e..b334e2f5a2 100644 --- a/pnpm11/pnpm/src/switchCliVersion.ts +++ b/pnpm11/pnpm/src/switchCliVersion.ts @@ -346,21 +346,12 @@ function installPnpmToStoreOptions ( envLockfile, storeController: store.ctrl, storeDir: store.dir, - registriesByScope: getPackageManagerBootstrapConfig(config).registriesByScope, virtualStoreDirMaxLength: config.virtualStoreDirMaxLength, packageManager: { name: packageManager.name, version: packageManager.version }, - // Network settings so the engine identity check can reach the canonical - // npm registry through the user's proxy / TLS configuration. - ca: config.ca, - cert: config.cert, - key: config.key, - httpProxy: config.httpProxy, - httpsProxy: config.httpsProxy, - noProxy: config.noProxy, - strictSsl: config.strictSsl, - localAddress: config.localAddress, + // The engine identity check reaches the registry with the same trusted + // network settings as the store controller that downloads pnpm. + ...getPackageManagerBootstrapConfig(config), maxSockets: config.maxSockets, - configByUri: config.configByUri, timeout: config.fetchTimeout, } }