diff --git a/packages/aws-cdk/lib/cli/proxy-agent.ts b/packages/aws-cdk/lib/cli/proxy-agent.ts index 44d596559..cef2823df 100644 --- a/packages/aws-cdk/lib/cli/proxy-agent.ts +++ b/packages/aws-cdk/lib/cli/proxy-agent.ts @@ -1,7 +1,35 @@ +import { ToolkitError } from '@aws-cdk/toolkit-lib'; import * as fs from 'fs-extra'; -import { ProxyAgent } from 'proxy-agent'; +import { ProxyAgent, proxies } from 'proxy-agent'; import type { IoHelper } from '../api-private'; +/** + * Validate a proxy address up front. + * + * `proxy-agent` only rejects an address with a missing or unsupported protocol + * lazily, on the first request, so without `-vvv` the CLI appears to hang or + * fails later with a misleading error (e.g. missing credentials). Fail fast + * here with an actionable message instead. + */ +export function validateProxyAddress(proxyAddress: string): void { + let protocol: string; + try { + protocol = new URL(proxyAddress).protocol.replace(/:$/, ''); + } catch { + throw new ToolkitError( + 'InvalidProxyAddress', + `Invalid proxy address '${proxyAddress}': it must be a URL that includes a protocol, e.g. 'http://${proxyAddress}'.`, + ); + } + + if (!(protocol in proxies)) { + throw new ToolkitError( + 'InvalidProxyAddress', + `Unsupported protocol '${protocol}' in proxy address '${proxyAddress}'. Supported protocols are: ${Object.keys(proxies).join(', ')}.`, + ); + } +} + /** * Options for proxy-agent SDKs */ @@ -29,6 +57,14 @@ export class ProxyAgentProvider { } public async create(options: ProxyAgentOptions) { + // Only validate when an actual proxy address was configured. When `--proxy` + // is not given the setting is unset (and can surface at runtime as an empty + // string or empty array), in which case we skip validation and let + // ProxyAgent fall back to environment-variable detection. + if (typeof options.proxyAddress === 'string' && options.proxyAddress.length > 0) { + validateProxyAddress(options.proxyAddress); + } + // Force it to use the proxy provided through the command line. // Otherwise, let the ProxyAgent auto-detect the proxy using environment variables. const getProxyForUrl = options.proxyAddress != null diff --git a/packages/aws-cdk/test/cli/proxy-agent.test.ts b/packages/aws-cdk/test/cli/proxy-agent.test.ts new file mode 100644 index 000000000..06f1efec5 --- /dev/null +++ b/packages/aws-cdk/test/cli/proxy-agent.test.ts @@ -0,0 +1,49 @@ +import { ProxyAgentProvider, validateProxyAddress } from '../../lib/cli/proxy-agent'; +import { TestIoHost } from '../_helpers/io-host'; + +describe('validateProxyAddress', () => { + test.each([ + 'http://localhost:1234', + 'https://proxy.example.com:8080', + 'socks5://localhost:1080', + 'pac+http://localhost/proxy.pac', + ])('accepts a proxy address with a supported protocol: %s', (address) => { + expect(() => validateProxyAddress(address)).not.toThrow(); + }); + + test.each([ + 'localhost:1234', + '1.2.3.4:8080', + 'proxy.example.com', + ])('rejects a proxy address without a usable protocol: %s', (address) => { + expect(() => validateProxyAddress(address)).toThrow(/proxy address/i); + }); + + test('rejects a proxy address with an unsupported protocol', () => { + expect(() => validateProxyAddress('ftp://localhost:1234')).toThrow(/Unsupported protocol/i); + }); +}); + +describe('ProxyAgentProvider', () => { + const ioHost = new TestIoHost(); + + test('create() fails fast with a clear error when the proxy address has no protocol', async () => { + const provider = new ProxyAgentProvider(ioHost.asHelper('deploy')); + await expect(provider.create({ proxyAddress: 'localhost:1234' })).rejects.toThrow(/proxy address/i); + }); + + test('create() succeeds with a valid proxy address', async () => { + const provider = new ProxyAgentProvider(ioHost.asHelper('deploy')); + await expect(provider.create({ proxyAddress: 'http://localhost:1234' })).resolves.toBeDefined(); + }); + + test.each([ + ['undefined', undefined], + ['an empty string', ''], + // Settings.get() can surface an unset value as an empty array at runtime. + ['an empty array', [] as unknown as string], + ])('create() does not validate when the proxy address is %s (no --proxy given)', async (_desc, proxyAddress) => { + const provider = new ProxyAgentProvider(ioHost.asHelper('deploy')); + await expect(provider.create({ proxyAddress })).resolves.toBeDefined(); + }); +});