From 396c2417d95639e858f6e11970ded09817e9e01c Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 10 Oct 2017 12:54:41 -0700 Subject: [PATCH] fix(service-worker): freshness strategy should clone response for cache (#19764) When Cache.put() is called with a Response, it consumes the response. If the Response is used for any other purpose (such as satisfying the original FetchEvent) it must be cloned first. A bug exists in the mocks used for SW tests, where this condition is not validated. The bodies of MockResponses can be utilized repeatedly without erroring in the same way that a real browser would. This bug is fixed by this commit, which causes tests for the freshness strategy of data caching to start failing. The cause of this failure is a second bug in the data caching code, where the Response is not cloned prior to being passed to Cache.put(). This is also fixed. PR Close #19764 --- packages/service-worker/worker/src/data.ts | 10 +++++----- packages/service-worker/worker/testing/cache.ts | 5 +++++ packages/service-worker/worker/testing/fetch.ts | 13 ++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/service-worker/worker/src/data.ts b/packages/service-worker/worker/src/data.ts index 2fce467d8f..5608611888 100644 --- a/packages/service-worker/worker/src/data.ts +++ b/packages/service-worker/worker/src/data.ts @@ -370,8 +370,7 @@ export class DataGroup { } // The request completed in time, so cache it inline with the response flow. - // Make sure to clone it so the real response can still be returned to the user. - await this.cacheResponse(req, res.clone(), lru); + await this.cacheResponse(req, res, lru); return res; } @@ -409,7 +408,7 @@ export class DataGroup { // No response in the cache. No choice but to fall back on the full network fetch. res = await networkFetch; - await this.cacheResponse(req, res.clone(), lru, true); + await this.cacheResponse(req, res, lru, true); return res; } @@ -517,8 +516,9 @@ export class DataGroup { // until enough other resources are requested that it falls off the end of the LRU chain. lru.accessed(req.url); - // Store the response in the cache. - await(await this.cache).put(req, res); + // Store the response in the cache (cloning because the browser will consume + // the body during the caching operation). + await(await this.cache).put(req, res.clone()); // Store the age of the cache. const ageTable = await this.ageTable; diff --git a/packages/service-worker/worker/testing/cache.ts b/packages/service-worker/worker/testing/cache.ts index b08428e20f..0c4a730bc9 100644 --- a/packages/service-worker/worker/testing/cache.ts +++ b/packages/service-worker/worker/testing/cache.ts @@ -138,6 +138,11 @@ export class MockCache implements Cache { async put(request: RequestInfo, response: Response): Promise { const url = (typeof request === 'string' ? request : request.url); this.cache.set(url, response.clone()); + + // Even though the body above is cloned, consume it here because the + // real cache consumes the body. + await response.text(); + return; } diff --git a/packages/service-worker/worker/testing/fetch.ts b/packages/service-worker/worker/testing/fetch.ts index 8c342dd1cc..aeacf1a4c3 100644 --- a/packages/service-worker/worker/testing/fetch.ts +++ b/packages/service-worker/worker/testing/fetch.ts @@ -12,7 +12,7 @@ export class MockBody implements Body { constructor(public _body: string|null) {} async arrayBuffer(): Promise { - this.bodyUsed = true; + this.markBodyUsed(); if (this._body !== null) { const buffer = new ArrayBuffer(this._body.length); const access = new Uint8Array(buffer); @@ -28,7 +28,7 @@ export class MockBody implements Body { async blob(): Promise { throw 'Not implemented'; } async json(): Promise { - this.bodyUsed = true; + this.markBodyUsed(); if (this._body !== null) { return JSON.parse(this._body); } else { @@ -37,7 +37,7 @@ export class MockBody implements Body { } async text(): Promise { - this.bodyUsed = true; + this.markBodyUsed(); if (this._body !== null) { return this._body; } else { @@ -46,6 +46,13 @@ export class MockBody implements Body { } async formData(): Promise { throw 'Not implemented'; } + + private markBodyUsed(): void { + if (this.bodyUsed === true) { + throw new Error('Cannot reuse body without cloning.'); + } + this.bodyUsed = true; + } } export class MockHeaders implements Headers {