fix(cr network): emit sw-handled requests when routing is enabled (#23589)

Previously, such requests were skipped because they never receive
`Fetch.requestPaused` as there was no real network.

Also cleanup some redundant tests and move them from chromium-only file.

Fixes #23424.
This commit is contained in:
Dmitry Gozman 2023-06-08 10:33:28 -07:00 committed by GitHub
parent 54a35dd108
commit 5cfd6d9fe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 172 deletions

View File

@ -384,16 +384,12 @@ export class CRNetworkManager {
// For frame-level Requests that are handled by a Service Worker's fetch handler, we'll never get a requestPaused event, so we need to
// manually create the request. In an ideal world, crNetworkManager would be able to know this on Network.requestWillBeSent, but there
// is not enough metadata there.
//
// PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS we guard with, since this would fix an old bug where, when using routing,
// request would not be emitted to the user for requests made by a page with a SW (and fetch handler) registered
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS && !request && event.response.fromServiceWorker) {
if (!request && event.response.fromServiceWorker) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
const frame = requestWillBeSentEvent?.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : null;
if (requestWillBeSentEvent && frame) {
this._onRequest(frame, requestWillBeSentEvent, null /* requestPausedPayload */);
request = this._requestIdToRequest.get(event.requestId);
if (requestWillBeSentEvent) {
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
this._onRequest(undefined, requestWillBeSentEvent, null /* requestPausedPayload */);
request = this._requestIdToRequest.get(event.requestId);
}
}
// FileUpload sends a response without a matching request.

View File

@ -0,0 +1,4 @@
<script>
window.registrationPromise = navigator.serviceWorker.register('sw.js');
window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
</script>

View File

@ -0,0 +1,29 @@
const kSwHtml = `
<script>
window.registrationPromise = navigator.serviceWorker.register('sw.js');
window.activationPromise = navigator.serviceWorker.controller ? Promise.resolve() : new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
window.fromSW = true;
</script>
`;
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('sw.html')) {
const blob = new Blob([kSwHtml], { type: 'text/html' });
const response = new Response(blob, { status: 200 , statusText: 'OK' });
event.respondWith(response);
return;
}
if (event.request.url.includes('error')) {
event.respondWith(Promise.reject(new Error('uh oh')));
return;
}
const slash = event.request.url.lastIndexOf('/');
const name = event.request.url.substring(slash + 1);
const blob = new Blob(['responseFromServiceWorker:' + name], { type: name.endsWith('.css') ? 'text/css' : 'application/javascript' });
const response = new Response(blob, {status: 200 , statusText: 'OK' });
event.respondWith(response);
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});

View File

@ -174,82 +174,6 @@ playwrightTest('should pass args with spaces', async ({ browserType, createUserD
expect(userAgent).toBe('I am Foo');
});
test.describe('should emit page-level network events with service worker fetch handler', () => {
test.describe('when not using routing', () => {
test('successful request', async ({ page, server }) => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq, pageResp, /* pageFinished */, swResponse] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('response'),
page.waitForEvent('requestfinished'),
page.evaluate(() => window['fetchDummy']('foo')),
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/);
expect(pageReq.serviceWorker()).toBe(null);
expect(pageResp.fromServiceWorker()).toBe(true);
expect(pageResp).toBe(await pageReq.response());
expect((await pageReq.response()).fromServiceWorker()).toBe(true);
});
test('failed request', async ({ page, server }) => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
});
});
test.describe('when routing', () => {
test('successful request', async ({ page, server, context }) => {
await context.route('**', route => route.continue());
await page.route('**', route => route.continue());
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [result, pageResp] = await Promise.all([
page.waitForEvent('request', { timeout: 750 }).catch(e => 'timeout'),
page.evaluate(() => window['fetchDummy']('foo')),
]);
expect(result).toBe('timeout');
expect(pageResp).toBeTruthy();
});
test('failed request', async ({ page, server, context }) => {
await context.route('**', route => route.continue());
let markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = false;
await page.route('**', route => {
if (route.request().url().endsWith('foo'))
markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = true;
void route.continue();
});
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
expect(markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker).toBe(false);
});
});
});
test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => {
test.skip(({ mode }) => mode !== 'default', 'Cannot set env variables in non-default');
test.beforeAll(() => process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1');
@ -692,94 +616,6 @@ test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => {
expect(error).toMatch(/REJECTED.*Failed to fetch/);
});
test.describe('should emit page-level network events with service worker fetch handler', () => {
test.describe('when not using routing', () => {
test('successful request', async ({ page, server }) => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq, pageResp, /* pageFinished */, swResponse] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('response'),
page.waitForEvent('requestfinished'),
page.evaluate(() => window['fetchDummy']('foo')),
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/);
expect(pageReq.serviceWorker()).toBe(null);
expect(pageResp.fromServiceWorker()).toBe(true);
expect(pageResp).toBe(await pageReq.response());
expect((await pageReq.response()).fromServiceWorker()).toBe(true);
});
test('failed request', async ({ page, server }) => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
});
});
test.describe('when routing', () => {
test('successful request', async ({ page, server, context }) => {
await context.route('**', route => route.continue());
let markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = false;
await page.route('**', route => {
if (route.request().url().endsWith('foo'))
markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = true;
void route.continue();
});
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq, pageResp, /* pageFinished */, swResponse] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('response'),
page.waitForEvent('requestfinished'),
page.evaluate(() => window['fetchDummy']('foo')),
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/);
expect(pageReq.serviceWorker()).toBe(null);
expect(pageResp.fromServiceWorker()).toBe(true);
expect(pageResp).toBe(await pageReq.response());
expect((await pageReq.response()).fromServiceWorker()).toBe(true);
expect(markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker).toBe(false);
});
test('failed request', async ({ page, server, context }) => {
await context.route('**', route => route.continue());
let markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = false;
await page.route('**', route => {
if (route.request().url().endsWith('foo'))
markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = true;
void route.continue();
});
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
expect(markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker).toBe(false);
});
});
});
test('setExtraHTTPHeaders', async ({ context, page, server }) => {
const [worker] = await Promise.all([
context.waitForEvent('serviceworker'),

View File

@ -53,9 +53,107 @@ it('should report requests and responses handled by service worker', async ({ pa
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(request.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/foo');
expect(request.serviceWorker()).toBe(null);
const response = await request.response();
expect(response.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/foo');
expect(await response.text()).toBe('responseFromServiceWorker:foo');
expect(response.fromServiceWorker()).toBe(true);
const [failedRequest] = await Promise.all([
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(failedRequest.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/error');
expect(failedRequest.failure()).not.toBe(null);
expect(failedRequest.serviceWorker()).toBe(null);
expect(await failedRequest.response()).toBe(null);
});
it('should report requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron }) => {
it.fixme(isAndroid);
it.fixme(isElectron);
await page.route('**/*', route => route.continue());
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [swResponse, request] = await Promise.all([
page.evaluate(() => window['fetchDummy']('foo')),
page.waitForEvent('request'),
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(request.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/foo');
expect(request.serviceWorker()).toBe(null);
const response = await request.response();
expect(response.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/foo');
expect(await response.text()).toBe('responseFromServiceWorker:foo');
const [failedRequest] = await Promise.all([
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(failedRequest.url()).toBe(server.PREFIX + '/serviceworkers/fetchdummy/error');
expect(failedRequest.failure()).not.toBe(null);
expect(failedRequest.serviceWorker()).toBe(null);
expect(await failedRequest.response()).toBe(null);
});
it('should report navigation requests and responses handled by service worker', async ({ page, server, isAndroid, isElectron, browserName }) => {
it.fixme(isAndroid);
it.fixme(isElectron);
await page.goto(server.PREFIX + '/serviceworkers/stub/sw.html');
await page.evaluate(() => window['activationPromise']);
const reloadResponse = await page.reload();
expect(await page.evaluate('window.fromSW')).toBe(true);
expect(reloadResponse.url()).toBe(server.PREFIX + '/serviceworkers/stub/sw.html');
await page.evaluate(() => window['activationPromise']);
if (browserName !== 'firefox') {
// When SW fetch throws, Firefox does not fail the navigation,
// but rather falls back to the real network.
const [, failedRequest] = await Promise.all([
page.evaluate(() => {
window.location.href = '/serviceworkers/stub/error.html';
}),
page.waitForEvent('requestfailed'),
]);
expect(failedRequest.url()).toBe(server.PREFIX + '/serviceworkers/stub/error.html');
expect(failedRequest.failure().errorText).toContain(browserName === 'chromium' ? 'net::ERR_FAILED' : 'uh oh');
expect(failedRequest.serviceWorker()).toBe(null);
expect(await failedRequest.response()).toBe(null);
}
});
it('should report navigation requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron, browserName }) => {
it.fixme(isAndroid);
it.fixme(isElectron);
await page.route('**/*', route => route.continue());
await page.goto(server.PREFIX + '/serviceworkers/stub/sw.html');
await page.evaluate(() => window['activationPromise']);
const reloadResponse = await page.reload();
expect(await page.evaluate('window.fromSW')).toBe(true);
expect(reloadResponse.url()).toBe(server.PREFIX + '/serviceworkers/stub/sw.html');
await page.evaluate(() => window['activationPromise']);
if (browserName !== 'firefox') {
// When SW fetch throws, Firefox does not fail the navigation,
// but rather falls back to the real network.
const [, failedRequest] = await Promise.all([
page.evaluate(() => {
window.location.href = '/serviceworkers/stub/error.html';
}),
page.waitForEvent('requestfailed'),
]);
expect(failedRequest.url()).toBe(server.PREFIX + '/serviceworkers/stub/error.html');
expect(failedRequest.failure().errorText).toContain(browserName === 'chromium' ? 'net::ERR_FAILED' : 'uh oh');
expect(failedRequest.serviceWorker()).toBe(null);
expect(await failedRequest.response()).toBe(null);
}
});
it('should return response body when Cross-Origin-Opener-Policy is set', async ({ page, server, browserName }) => {