Skip to content

Commit b2e4c72

Browse files
authored
Ensure bad responses aren't cached (#2738)
* Don't cache bad responses * Allow cacheWillUpdate to fail install * Linting * Typos
1 parent 590d126 commit b2e4c72

File tree

3 files changed

+332
-43
lines changed

3 files changed

+332
-43
lines changed

packages/workbox-precaching/src/PrecacheStrategy.ts

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ import {StrategyHandler} from 'workbox-strategies/StrategyHandler.js';
1717

1818
import './_version.js';
1919

20-
21-
const copyRedirectedCacheableResponsesPlugin: WorkboxPlugin = {
22-
async cacheWillUpdate({response}) {
23-
return response.redirected ? await copyResponse(response) : response;
24-
}
25-
}
26-
2720
interface PrecacheStrategyOptions extends StrategyOptions {
2821
fallbackToNetwork?: boolean;
2922
}
@@ -43,6 +36,22 @@ interface PrecacheStrategyOptions extends StrategyOptions {
4336
class PrecacheStrategy extends Strategy {
4437
private readonly _fallbackToNetwork: boolean;
4538

39+
static readonly defaultPrecacheCacheabilityPlugin: WorkboxPlugin = {
40+
async cacheWillUpdate({response}) {
41+
if (!response || response.status >= 400) {
42+
return null;
43+
}
44+
45+
return response;
46+
}
47+
};
48+
49+
static readonly copyRedirectedCacheableResponsesPlugin: WorkboxPlugin = {
50+
async cacheWillUpdate({response}) {
51+
return response.redirected ? await copyResponse(response) : response;
52+
}
53+
};
54+
4655
/**
4756
*
4857
* @param {Object} [options]
@@ -70,7 +79,7 @@ class PrecacheStrategy extends Strategy {
7079
// any redirected response must be "copied" rather than cloned, so the new
7180
// response doesn't contain the `redirected` flag. See:
7281
// https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1
73-
this.plugins.push(copyRedirectedCacheableResponsesPlugin);
82+
this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
7483
}
7584

7685
/**
@@ -140,20 +149,14 @@ class PrecacheStrategy extends Strategy {
140149
}
141150

142151
async _handleInstall(request: Request, handler: StrategyHandler) {
143-
const response = await handler.fetchAndCachePut(request);
144-
145-
// Any time there's no response, consider it a precaching error.
146-
let responseSafeToPrecache = Boolean(response);
152+
this._useDefaultCacheabilityPluginIfNeeded();
147153

148-
// Also consider it an error if the user didn't pass their own
149-
// cacheWillUpdate plugin, and the response is a 400+ (note: this means
150-
// that by default opaque responses can be precached).
151-
if (response && response.status >= 400 &&
152-
!this._usesCustomCacheableResponseLogic()) {
153-
responseSafeToPrecache = false;
154-
}
154+
const response = await handler.fetch(request);
155155

156-
if (!responseSafeToPrecache) {
156+
// Make sure we defer cachePut() until after we know the response
157+
// should be cached; see https://github.com/GoogleChrome/workbox/issues/2737
158+
const wasCached = await handler.cachePut(request, response.clone());
159+
if (!wasCached) {
157160
// Throwing here will lead to the `install` handler failing, which
158161
// we want to do if *any* of the responses aren't safe to cache.
159162
throw new WorkboxError('bad-precaching-response', {
@@ -166,19 +169,59 @@ class PrecacheStrategy extends Strategy {
166169
}
167170

168171
/**
169-
* Returns true if any users plugins were added containing their own
170-
* `cacheWillUpdate` callback.
171-
*
172-
* This method indicates whether the default cacheable response logic (i.e.
173-
* <400, including opaque responses) should be used. If a custom plugin
174-
* with a `cacheWillUpdate` callback is passed, then the strategy should
175-
* defer to that plugin's logic.
172+
* This method is complex, as there a number of things to account for:
173+
*
174+
* The `plugins` array can be set at construction, and/or it might be added to
175+
* to at any time before the strategy is used.
176+
*
177+
* At the time the strategy is used (i.e. during an `install` event), there
178+
* needs to be at least one plugin that implements `cacheWillUpdate` in the
179+
* array, other than `copyRedirectedCacheableResponsesPlugin`.
180+
*
181+
* - If this method is called and there are no suitable `cacheWillUpdate`
182+
* plugins, we need to add `defaultPrecacheCacheabilityPlugin`.
183+
*
184+
* - If this method is called and there is exactly one `cacheWillUpdate`, then
185+
* we don't have to do anything (this might be a previously added
186+
* `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin).
187+
*
188+
* - If this method is called and there is more than one `cacheWillUpdate`,
189+
* then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so,
190+
* we need to remove it. (This situation is unlikely, but it could happen if
191+
* the strategy is used multiple times, the first without a `cacheWillUpdate`,
192+
* and then later on after manually adding a custom `cacheWillUpdate`.)
193+
*
194+
* See https://github.com/GoogleChrome/workbox/issues/2737 for more context.
176195
*
177196
* @private
178197
*/
179-
_usesCustomCacheableResponseLogic(): boolean {
180-
return this.plugins.some((plugin) => plugin.cacheWillUpdate &&
181-
plugin !== copyRedirectedCacheableResponsesPlugin);
198+
_useDefaultCacheabilityPluginIfNeeded(): void {
199+
let defaultPluginIndex: number | null = null;
200+
let cacheWillUpdatePluginCount = 0;
201+
202+
for (const [index, plugin] of this.plugins.entries()) {
203+
// Ignore the copy redirected plugin when determining what to do.
204+
if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
205+
continue;
206+
}
207+
208+
// Save the default plugin's index, in case it needs to be removed.
209+
if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
210+
defaultPluginIndex = index;
211+
}
212+
213+
if (plugin.cacheWillUpdate) {
214+
cacheWillUpdatePluginCount++;
215+
}
216+
}
217+
218+
if (cacheWillUpdatePluginCount === 0) {
219+
this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
220+
} else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
221+
// Only remove the default plugin; multiple custom plugins are allowed.
222+
this.plugins.splice(defaultPluginIndex, 1);
223+
}
224+
// Nothing needs to be done if cacheWillUpdatePluginCount is 1
182225
}
183226
}
184227

packages/workbox-strategies/src/StrategyHandler.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,11 @@ class StrategyHandler {
302302
* - cacheDidUpdate()
303303
*
304304
* @param {Request|string} key The request or URL to use as the cache key.
305-
* @param {Promise<void>} response The response to cache.
305+
* @param {Response} response The response to cache.
306+
* @return {Promise<boolean>} `false` if a cacheWillUpdate caused the response
307+
* not be cached, and `true` otherwise.
306308
*/
307-
async cachePut(key: RequestInfo, response: Response): Promise<void> {
309+
async cachePut(key: RequestInfo, response: Response): Promise<boolean> {
308310
const request: Request = toRequest(key);
309311

310312
// Run in the next task to avoid blocking other cache reads.
@@ -340,7 +342,7 @@ class StrategyHandler {
340342
logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` +
341343
`will not be cached.`, responseToCache);
342344
}
343-
return;
345+
return false;
344346
}
345347

346348
const {cacheName, matchOptions} = this._strategy;
@@ -379,6 +381,8 @@ class StrategyHandler {
379381
event: this.event,
380382
});
381383
}
384+
385+
return true;
382386
}
383387

384388
/**

0 commit comments

Comments
 (0)