diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index b154ce63079fb..d996f02cf5b68 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -90,6 +90,7 @@ export type APIRequestFinishedEvent = { type SendRequestOptions = https.RequestOptions & { maxRedirects: number, headers: HeadersObject, + proxy?: types.ProxySettings, __testHookLookup?: (hostname: string) => LookupAddress[] }; @@ -221,12 +222,9 @@ export abstract class APIRequestContext extends SdkObject { setBasicAuthorizationHeader(headers, credentials); const method = params.method?.toUpperCase() || 'GET'; - const proxy = defaults.proxy; - let agent; // We skip 'per-context' in order to not break existing users. 'per-context' was previously used to // workaround an upstream Chromium bug. Can be removed in the future. - if (proxy?.server !== 'per-context') - agent = createProxyAgent(proxy, requestUrl); + const proxy = defaults.proxy?.server !== 'per-context' ? defaults.proxy : undefined; let maxRedirects = params.maxRedirects ?? (defaults.maxRedirects ?? 20); maxRedirects = maxRedirects === 0 ? -1 : maxRedirects; @@ -234,7 +232,7 @@ export abstract class APIRequestContext extends SdkObject { const options: SendRequestOptions = { method, headers, - agent, + proxy, maxRedirects, ...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin), __testHookLookup: (params as any).__testHookLookup, @@ -362,11 +360,11 @@ export abstract class APIRequestContext extends SdkObject { const resultPromise = new Promise((fulfill, reject) => { const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest) = (url.protocol === 'https:' ? https : http).request; - // Without an explicit proxy agent, use this context's own agent, which has - // keep-alive enabled and connects with Happy Eyeballs (autoSelectFamily). - // Resolved per request so that a cross-protocol redirect picks the right agent. + // Without a proxy, use this context's own agent, which has keep-alive enabled + // and connects with Happy Eyeballs (autoSelectFamily). Resolved per request so + // that each redirect hop evaluates proxy bypass and picks the right agent. const requestOptions = { ...options, ...happyEyeballsOptions }; - requestOptions.agent = options.agent ?? this._ensureAgent(url.protocol); + requestOptions.agent = createProxyAgent(options.proxy, url) ?? this._ensureAgent(url.protocol); if (options.__testHookLookup) requestOptions.lookup = lookupWithTestHook(options.__testHookLookup); @@ -484,7 +482,7 @@ export abstract class APIRequestContext extends SdkObject { const redirectOptions: SendRequestOptions = { method, headers, - agent: options.agent, + proxy: options.proxy, maxRedirects: options.maxRedirects - 1, __testHookLookup: options.__testHookLookup, }; diff --git a/tests/library/fetch-proxy.spec.ts b/tests/library/fetch-proxy.spec.ts index 5d35bef85b063..2e0ac34f6f22a 100644 --- a/tests/library/fetch-proxy.spec.ts +++ b/tests/library/fetch-proxy.spec.ts @@ -130,6 +130,31 @@ it(`should support proxy.bypass`, async ({ contextFactory, contextOptions, serve } }); +it('should evaluate proxy.bypass for each redirect', async ({ contextFactory, contextOptions, server, proxyServer }) => { + proxyServer.forwardTo(server.PORT, { allowConnectRequests: true }); + server.setRedirect('/redirect-to-cross-process', server.CROSS_PROCESS_PREFIX + '/simple.json'); + server.setRedirect('/redirect-to-same-process', server.PREFIX + '/simple.json'); + const context = await contextFactory({ + ...contextOptions, + proxy: { server: `localhost:${proxyServer.PORT}`, bypass: new URL(server.PREFIX).hostname }, + }); + + { + // Bypassed first hop, redirect target goes through the proxy. + const response = await context.request.get(server.PREFIX + '/redirect-to-cross-process'); + expect(response.url()).toBe(server.CROSS_PROCESS_PREFIX + '/simple.json'); + expect(proxyServer.connectHosts).toEqual([new URL(server.CROSS_PROCESS_PREFIX).host]); + proxyServer.connectHosts = []; + } + + { + // First hop through the proxy, bypassed redirect target goes direct. + const response = await context.request.get(server.CROSS_PROCESS_PREFIX + '/redirect-to-same-process'); + expect(response.url()).toBe(server.PREFIX + '/simple.json'); + expect(proxyServer.connectHosts).toEqual([new URL(server.CROSS_PROCESS_PREFIX).host]); + } +}); + it('should use socks proxy', async ({ playwright, server, socksPort }) => { it.skip(!!process.env.INSIDE_DOCKER, 'connect ECONNREFUSED 127.0.0.1:');