From 2d3e4027e777e5b4fc803e1fb9330ad186516c79 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 7 Feb 2023 15:10:44 -0800 Subject: [PATCH] fix(chromium): do not pre-populate non-preflight OPTIONS requests (#20684) Fixes #20469. --- .../src/server/chromium/crNetworkManager.ts | 8 +-- tests/page/page-route.spec.ts | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index b2a67da39d..35069d958d 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -217,12 +217,12 @@ export class CRNetworkManager { frame = this._page._frameManager.frameAttached(requestWillBeSentEvent.frameId, null); } - // CORS options request is generated by the network stack. If interception is enabled, + // CORS options preflight request is generated by the network stack. If interception is enabled, // we accept all CORS options, assuming that this was intended when setting route. // - // Note: it would be better to match the URL against interception patterns, but - // that information is only available to the client. Perhaps we can just route to the client? - if (requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && (this._page || this._serviceWorker)!.needsRequestInterception()) { + // Note: it would be better to match the URL against interception patterns. + const isInterceptedOptionsPreflight = !!requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && requestWillBeSentEvent.initiator.type === 'preflight'; + if (isInterceptedOptionsPreflight && (this._page || this._serviceWorker)!.needsRequestInterception()) { const requestHeaders = requestPausedEvent.request.headers; const responseHeaders: Protocol.Fetch.HeaderEntry[] = [ { name: 'Access-Control-Allow-Origin', value: requestHeaders['Origin'] || '*' }, diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 764d24f06d..6509fd2938 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -688,6 +688,63 @@ it('should respect cors overrides', async ({ page, server, browserName, isAndroi } }); +it('should not auto-intercept non-preflight OPTIONS', async ({ page, server, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20469' }); + + await page.goto(server.EMPTY_PAGE); + + let requests = []; + server.setRoute('/something', (request, response) => { + requests.push(request.method + ':' + request.url); + if (request.method === 'OPTIONS') { + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Allow-Headers': '*', + 'Cache-Control': 'no-cache' + }); + response.end(`Hello`); + return; + } + response.writeHead(200, { 'Access-Control-Allow-Origin': '*' }); + response.end('World'); + }); + + // Without interception. + { + requests = []; + const [text1, text2] = await page.evaluate(async url => { + const response1 = await fetch(url, { method: 'OPTIONS' }); + const text1 = await response1.text(); + const response2 = await fetch(url, { method: 'GET' }); + const text2 = await response2.text(); + return [text1, text2]; + }, server.CROSS_PROCESS_PREFIX + '/something'); + expect.soft(text1).toBe('Hello'); + expect.soft(text2).toBe('World'); + // Preflight for OPTIONS, then OPTIONS, then GET without preflight. + expect.soft(requests).toEqual(['OPTIONS:/something', 'OPTIONS:/something', 'GET:/something']); + } + + // With interception. + { + await page.route('**/something', route => route.continue()); + + requests = []; + const [text1, text2] = await page.evaluate(async url => { + const response1 = await fetch(url, { method: 'OPTIONS' }); + const text1 = await response1.text(); + const response2 = await fetch(url, { method: 'GET' }); + const text2 = await response2.text(); + return [text1, text2]; + }, server.CROSS_PROCESS_PREFIX + '/something'); + expect.soft(text1).toBe('Hello'); + expect.soft(text2).toBe('World'); + // Preflight for OPTIONS is auto-fulfilled, then OPTIONS, then GET without preflight. + expect.soft(requests).toEqual(['OPTIONS:/something', 'GET:/something']); + } +}); + it('should support cors with POST', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); await page.route('**/cars', async route => {