fix(service-worker): correctly determine client ID on navigation requests (#42607)

The ServiceWorker assigns an app-version to a each client to ensure that
all subsequent requests for a client are served using the same
app-version. The assignment is done based on the client ID.

Previously, the ServiceWorker would only try to read the client's ID off
of the `FetchEvent`'s `clientId` property. However, for navigation
requests the new client's ID will be set on [resultingClientId][1],
while `clientId` will either be empty or hold the ID of the client where
the request initiated from. See also related discussions in
w3c/ServiceWorker#870 and w3c/ServiceWorker#1266.

In theory, this could lead to the navigation request (i.e. `index.html`)
being served from a different app-version than the subsequent
sub-resource requests (i.e. assets). In practice, the likelihood of this
happening is probably very low though, since it would require the latest
app-version to be updated between the initial navigation request and the
first sub-resource request, which should happen very shortly after the
navigation request.

This commit ensures that the correct client ID is determined even for
navigation requests by also taking the `resultingClientId` property into
account.

[1]: https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/resultingClientId

PR Close #42607
This commit is contained in:
George Kalpakas 2021-06-22 16:09:39 +03:00 committed by Dylan Hunn
parent 81a19e4e65
commit 9498da1038
4 changed files with 15 additions and 12 deletions

View File

@ -609,7 +609,10 @@ export class Driver implements Debuggable, UpdateSource {
private async assignVersion(event: FetchEvent): Promise<AppVersion|null> {
// First, check whether the event has a (non empty) client ID. If it does, the version may
// already be associated.
const clientId = event.clientId;
//
// NOTE: For navigation requests, we care about the `resultingClientId`. If it is undefined or
// the empty string (which is the case for sub-resource requests), we look at `clientId`.
const clientId = event.resultingClientId || event.clientId;
if (clientId) {
// Check if there is an assigned client id.
if (this.clientVersionMap.has(clientId)) {

View File

@ -60,8 +60,9 @@ type WindowClientState = 'hidden'|'visible'|'prerender'|'unloaded';
// Fetch API
interface FetchEvent extends ExtendableEvent {
clientId: string|null;
request: Request;
clientId?: string;
resultingClientId?: string;
respondWith(response: Promise<Response>|Response): Promise<Response>;
}

View File

@ -538,7 +538,6 @@ describe('Driver', () => {
it('handles empty client ID', async () => {
// Initialize the SW.
expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo');
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo');
await driver.initialized;
// Update to a new version.
@ -547,7 +546,6 @@ describe('Driver', () => {
// Correctly handle navigation requests, even if `clientId` is null/empty.
expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo v2');
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2');
});
it('checks for updates on restart', async () => {
@ -2008,8 +2006,7 @@ async function removeAssetFromCache(
}
async function makeRequest(
scope: SwTestHarness, url: string, clientId: string|null = 'default',
init?: Object): Promise<string|null> {
scope: SwTestHarness, url: string, clientId = 'default', init?: Object): Promise<string|null> {
const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId);
await done;
const res = await resPromise;
@ -2020,8 +2017,7 @@ async function makeRequest(
}
function makeNavigationRequest(
scope: SwTestHarness, url: string, clientId?: string|null,
init: Object = {}): Promise<string|null> {
scope: SwTestHarness, url: string, clientId?: string, init: Object = {}): Promise<string|null> {
return makeRequest(scope, url, clientId, {
headers: {
Accept: 'text/plain, text/html, text/css',

View File

@ -219,12 +219,14 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
waitUntil(promise: Promise<void>): void {}
handleFetch(req: Request, clientId: string|null = null):
[Promise<Response|undefined>, Promise<void>] {
handleFetch(req: Request, clientId = ''): [Promise<Response|undefined>, Promise<void>] {
if (!this.eventHandlers.has('fetch')) {
throw new Error('No fetch handler registered');
}
const event = new MockFetchEvent(req, clientId);
const isNavigation = req.mode === 'navigate';
const event = isNavigation ? new MockFetchEvent(req, '', clientId) :
new MockFetchEvent(req, clientId, '');
this.eventHandlers.get('fetch')!.call(this, event);
if (clientId) {
@ -373,7 +375,8 @@ class MockExtendableEvent extends OneTimeContext {}
class MockFetchEvent extends MockExtendableEvent {
response: Promise<Response|undefined> = Promise.resolve(undefined);
constructor(readonly request: Request, readonly clientId: string|null) {
constructor(
readonly request: Request, readonly clientId: string, readonly resultingClientId: string) {
super();
}