From 6e81c1d91572ac662ce298818a36f948b06ea566 Mon Sep 17 00:00:00 2001 From: "Abnoz.v2" Date: Sun, 20 Sep 2026 11:24:21 +0100 Subject: [PATCH] fix(client-certificates): honor browser-level proxy and proxy.bypass The client certificates interceptor replaces the browser proxy with a local SOCKS proxy, so it has to apply the user's proxy settings itself. It only received the context-level proxy and never evaluated bypass. Fixes https://github.com/microsoft/playwright/issues/42806 --- .../playwright-core/src/server/browser.ts | 3 +- .../socksClientCertificatesInterceptor.ts | 8 ++-- tests/library/client-certificates.spec.ts | 41 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 53f6363221e5d..9e9f45613880f 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -108,7 +108,8 @@ export abstract class Browser extends SdkObject { let context: BrowserContext | undefined; try { if (options.clientCertificates?.length) { - clientCertificatesProxy = await ClientCertificatesProxy.create(progress, options); + // The interceptor replaces the browser proxy, so it must apply the browser-level proxy itself. + clientCertificatesProxy = await ClientCertificatesProxy.create(progress, { ...options, proxy: options.proxy ?? this.options.proxy }); options = { ...options }; options.proxyOverride = clientCertificatesProxy.proxySettings(); options.internalIgnoreHTTPSErrors = true; diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 01aa7a3a14ba0..fada93c8df379 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -287,7 +287,8 @@ export class ClientCertificatesProxy { ) { verifyClientCertificates(contextOptions.clientCertificates); this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors; - this._proxy = contextOptions.proxy; + // 'per-context' is a Chromium launch-time placeholder, not a real proxy. + this._proxy = contextOptions.proxy?.server === 'per-context' ? undefined : contextOptions.proxy; this._initSecureContexts(contextOptions.clientCertificates); this._socksProxy = new SocksProxy(); this._socksProxy.setPattern('*'); @@ -312,9 +313,8 @@ export class ClientCertificatesProxy { } _getProxyAgent(host: string, port: number) { - const proxyFromOptions = createProxyAgent(this._proxy); - if (proxyFromOptions) - return proxyFromOptions; + if (this._proxy) + return createProxyAgent(this._proxy, new URL(`https://${host.includes(':') ? `[${host}]` : host}:${port}`)); const proxyFromEnv = getProxyForUrl(`https://${host}:${port}`); if (proxyFromEnv) return createProxyAgent({ server: proxyFromEnv }); diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 6340d5d54297c..ad863cecdbbfa 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -498,6 +498,47 @@ test.describe('browser', () => { await page.close(); }); + test('should use the browser-level proxy with client certificates', async ({ browserType, startCCServer, asset, browserName, proxyServer, isMac }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && isMac }); + proxyServer.forwardTo(parseInt(new URL(serverURL).port, 10), { allowConnectRequests: true }); + const browser = await browserType.launch({ proxy: { server: `localhost:${proxyServer.PORT}` } }); + try { + const page = await browser.newPage({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }); + expect(proxyServer.connectHosts).toEqual([]); + await page.goto(serverURL); + const host = browserName === 'webkit' && isMac ? 'localhost' : '127.0.0.1'; + expect([...new Set(proxyServer.connectHosts)]).toEqual([`${host}:${new URL(serverURL).port}`]); + await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); + } finally { + await browser.close(); + } + }); + + test('should respect proxy bypass with client certificates', async ({ browser, startCCServer, asset, browserName, proxyServer, isMac }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && isMac }); + proxyServer.forwardTo(parseInt(new URL(serverURL).port, 10), { allowConnectRequests: true }); + const page = await browser.newPage({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + proxy: { server: `localhost:${proxyServer.PORT}`, bypass: new URL(serverURL).hostname }, + }); + await page.goto(serverURL); + expect(proxyServer.connectHosts).toEqual([]); + await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); + await page.close(); + }); + test('should pass with matching certificates and when a http proxy is used from env', async ({ mode, browser, startCCServer, asset, browserName, proxyServer, isMac }) => { test.skip(mode !== 'default', 'Out of process transport does not allow us to set env vars dynamically'); process.env.HTTPS_PROXY = `http://localhost:${proxyServer.PORT}`;