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:
parent
fcfb1544e8
commit
396c2417d9
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue