Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type APIRequestFinishedEvent = {
type SendRequestOptions = https.RequestOptions & {
maxRedirects: number,
headers: HeadersObject,
proxy?: types.ProxySettings,
__testHookLookup?: (hostname: string) => LookupAddress[]
};

Expand Down Expand Up @@ -221,20 +222,17 @@ 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;

const options: SendRequestOptions = {
method,
headers,
agent,
proxy,
maxRedirects,
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
__testHookLookup: (params as any).__testHookLookup,
Expand Down Expand Up @@ -362,11 +360,11 @@ export abstract class APIRequestContext extends SdkObject {
const resultPromise = new Promise<SendRequestResult>((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);

Expand Down Expand Up @@ -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,
};
Expand Down
25 changes: 25 additions & 0 deletions tests/library/fetch-proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<port>');

Expand Down