diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 376dfbeca6..47ffc4ad9b 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -150,11 +150,16 @@ export abstract class APIRequestContext extends SdkObject { setHeader(headers, name, value); } + const requestUrl = new URL(params.url, defaults.baseURL); + if (params.params) { + for (const { name, value } of params.params) + requestUrl.searchParams.set(name, value); + } + const method = params.method?.toUpperCase() || 'GET'; const proxy = defaults.proxy; let agent; - if (proxy && proxy.server !== 'per-context') { - // TODO: support bypass proxy + if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) { const proxyOpts = url.parse(proxy.server); if (proxyOpts.protocol?.startsWith('socks')) { agent = new SocksProxyAgent({ @@ -184,12 +189,6 @@ export abstract class APIRequestContext extends SdkObject { if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors) options.rejectUnauthorized = false; - const requestUrl = new URL(params.url, defaults.baseURL); - if (params.params) { - for (const { name, value } of params.params) - requestUrl.searchParams.set(name, value); - } - const postData = serializePostData(params, headers); if (postData) setHeader(headers, 'content-length', String(postData.byteLength)); @@ -706,3 +705,16 @@ function getHeader(headers: HeadersObject, name: string) { function removeHeader(headers: { [name: string]: string }, name: string) { delete headers[name]; } + +function shouldBypassProxy(url: URL, bypass?: string): boolean { + if (!bypass) + return false; + const domains = bypass.split(',').map(s => { + s = s.trim(); + if (!s.startsWith('.')) + s = '.' + s; + return s; + }); + const domain = '.' + url.hostname; + return domains.some(d => domain.endsWith(d)); +} \ No newline at end of file diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 0093d709ec..1992a2e4a3 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -370,6 +370,58 @@ it('should pass proxy credentials', async ({ browserType, server, proxyServer }) await browser.close(); }); +it(`should support proxy.bypass`, async ({ browser, browserType, browserName, contextOptions, server, proxyServer }) => { + server.setRoute('/target.html', async (req, res) => { + res.end('Served by the proxy'); + }); + // FYI: using long and weird domain names to avoid ATT DNS hijacking + // that resolves everything to some weird search results page. + // + // @see https://gist.github.com/CollinChaffin/24f6c9652efb3d6d5ef2f5502720ef00 + proxyServer.forwardTo(server.PORT); + const context = await browser.newContext({ + ...contextOptions, + proxy: { server: `localhost:${proxyServer.PORT}`, bypass: `1.non.existent.domain.for.the.test, 2.non.existent.domain.for.the.test, .another.test` } + }); + + { + const res = await context.request.get(server.CROSS_PROCESS_PREFIX + '/target.html'); + expect(await res.text()).toContain('Served by the proxy'); + expect(proxyServer.connectHosts).toContain(new URL(server.CROSS_PROCESS_PREFIX).host); + proxyServer.connectHosts = []; + } + + { + const res = await context.request.get('http://0.non.existent.domain.for.the.test/target.html'); + expect(await res.text()).toContain('Served by the proxy'); + proxyServer.connectHosts = []; + } + + { + const error = await context.request.get('http://1.non.existent.domain.for.the.test/target.html').catch(e => e); + expect(error.message).toBeTruthy(); + expect(proxyServer.connectHosts).toEqual([]); + } + + { + const error = await context.request.get('http://2.non.existent.domain.for.the.test/target.html').catch(e => e); + expect(error.message).toBeTruthy(); + expect(proxyServer.connectHosts).toEqual([]); + } + + { + const error = await context.request.get('http://foo.is.the.another.test/target.html').catch(e => e); + expect(error.message).toBeTruthy(); + expect(proxyServer.connectHosts).toEqual([]); + } + + { + const res = await context.request.get('http://3.non.existent.domain.for.the.test/target.html'); + expect(await res.text()).toContain('Served by the proxy'); + } +}); + + it('should work with http credentials', async ({ context, server }) => { server.setAuth('/empty.html', 'user', 'pass');