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
This commit is contained in:
Alex Rickabaugh 2017-10-10 12:54:41 -07:00 committed by Tobias Bosch
parent fcfb1544e8
commit 396c2417d9
3 changed files with 20 additions and 8 deletions

View File

@ -370,8 +370,7 @@ export class DataGroup {
} }
// The request completed in time, so cache it inline with the response flow. // 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, lru);
await this.cacheResponse(req, res.clone(), lru);
return res; 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. // No response in the cache. No choice but to fall back on the full network fetch.
res = await networkFetch; res = await networkFetch;
await this.cacheResponse(req, res.clone(), lru, true); await this.cacheResponse(req, res, lru, true);
return res; 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. // until enough other resources are requested that it falls off the end of the LRU chain.
lru.accessed(req.url); lru.accessed(req.url);
// Store the response in the cache. // Store the response in the cache (cloning because the browser will consume
await(await this.cache).put(req, res); // the body during the caching operation).
await(await this.cache).put(req, res.clone());
// Store the age of the cache. // Store the age of the cache.
const ageTable = await this.ageTable; const ageTable = await this.ageTable;

View File

@ -138,6 +138,11 @@ export class MockCache implements Cache {
async put(request: RequestInfo, response: Response): Promise<void> { async put(request: RequestInfo, response: Response): Promise<void> {
const url = (typeof request === 'string' ? request : request.url); const url = (typeof request === 'string' ? request : request.url);
this.cache.set(url, response.clone()); 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; return;
} }

View File

@ -12,7 +12,7 @@ export class MockBody implements Body {
constructor(public _body: string|null) {} constructor(public _body: string|null) {}
async arrayBuffer(): Promise<ArrayBuffer> { async arrayBuffer(): Promise<ArrayBuffer> {
this.bodyUsed = true; this.markBodyUsed();
if (this._body !== null) { if (this._body !== null) {
const buffer = new ArrayBuffer(this._body.length); const buffer = new ArrayBuffer(this._body.length);
const access = new Uint8Array(buffer); const access = new Uint8Array(buffer);
@ -28,7 +28,7 @@ export class MockBody implements Body {
async blob(): Promise<Blob> { throw 'Not implemented'; } async blob(): Promise<Blob> { throw 'Not implemented'; }
async json(): Promise<any> { async json(): Promise<any> {
this.bodyUsed = true; this.markBodyUsed();
if (this._body !== null) { if (this._body !== null) {
return JSON.parse(this._body); return JSON.parse(this._body);
} else { } else {
@ -37,7 +37,7 @@ export class MockBody implements Body {
} }
async text(): Promise<string> { async text(): Promise<string> {
this.bodyUsed = true; this.markBodyUsed();
if (this._body !== null) { if (this._body !== null) {
return this._body; return this._body;
} else { } else {
@ -46,6 +46,13 @@ export class MockBody implements Body {
} }
async formData(): Promise<FormData> { throw 'Not implemented'; } async formData(): Promise<FormData> { 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 { export class MockHeaders implements Headers {