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 <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-26 14:37:43 +01:00
1 parent c70dd920af
commit e00bf41300
5 files changed
+147 -22

No files matched your search

@@ -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).
@@ -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<string, string | undefined>, 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
}
/**
+96
View File
@@ -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()
+22
View File
@@ -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 = {
+3 -12
View File
@@ -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,
}
}