feat(service-worker): use `ignoreVary: true` when retrieving responses from cache (#34663)
The Angular ServiceWorker always uses a copy of the request without headers for caching assets (in order to avoid issues with opaque responses). Therefore, it was previously not possible to retrieve resources from the cache if the response contained [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) headers. In addition to that, `Vary` headers do not work in all browsers (or work differently) and may not work as intended with ServiceWorker caches. See [this article](https://www.smashingmagazine.com/2017/11/understanding-vary-header) and the linked resources for more info. This commit avoids the aforementioned issues by making sure the Angular ServiceWorker always sets the `ignoreVary` option passed to [Cache#match()](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) to `true`. This allows the ServiceWorker to correctly retrieve cached responses with `Vary` headers, which was previously not possible. Fixes #36638 BREAKING CHANGE: Previously, [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) headers would be taken into account when retrieving resources from the cache, completely preventing the retrieval of cached assets (due to ServiceWorker implementation details) and leading to unpredictable behavior due to inconsistent/buggy implementations in different browsers. Now, `Vary` headers are ignored when retrieving resources from the ServiceWorker caches, which can result in resources being retrieved even when their headers are different. If your application needs to differentiate its responses based on request headers, please make sure the Angular ServiceWorker is [configured](https://angular.io/guide/service-worker-config) to avoid caching the affected resources. PR Close #34663
This commit is contained in:
parent
dc9f4b994e
commit
ee35e223a7
|
@ -153,6 +153,9 @@ function withOrderedKeys<T extends {[key: string]: any}>(unorderedObj: T): T {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCacheQueryOptions(inOptions?: Pick<CacheQueryOptions, 'ignoreSearch'>):
|
function buildCacheQueryOptions(inOptions?: Pick<CacheQueryOptions, 'ignoreSearch'>):
|
||||||
CacheQueryOptions|undefined {
|
CacheQueryOptions {
|
||||||
return inOptions;
|
return {
|
||||||
|
ignoreVary: true,
|
||||||
|
...inOptions,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ describe('Generator', () => {
|
||||||
'\\/some\\/url\\?with\\+escaped\\+chars',
|
'\\/some\\/url\\?with\\+escaped\\+chars',
|
||||||
'\\/test\\/relative\\/[^/]*\\.txt',
|
'\\/test\\/relative\\/[^/]*\\.txt',
|
||||||
],
|
],
|
||||||
cacheQueryOptions: undefined,
|
cacheQueryOptions: {ignoreVary: true}
|
||||||
}],
|
}],
|
||||||
dataGroups: [{
|
dataGroups: [{
|
||||||
name: 'other',
|
name: 'other',
|
||||||
|
@ -106,7 +106,7 @@ describe('Generator', () => {
|
||||||
maxAge: 259200000,
|
maxAge: 259200000,
|
||||||
timeoutMs: 60000,
|
timeoutMs: 60000,
|
||||||
version: 1,
|
version: 1,
|
||||||
cacheQueryOptions: undefined,
|
cacheQueryOptions: {ignoreVary: true}
|
||||||
}],
|
}],
|
||||||
navigationUrls: [
|
navigationUrls: [
|
||||||
{positive: true, regex: '^\\/included\\/absolute\\/.*$'},
|
{positive: true, regex: '^\\/included\\/absolute\\/.*$'},
|
||||||
|
@ -229,7 +229,7 @@ describe('Generator', () => {
|
||||||
'/main.js',
|
'/main.js',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
cacheQueryOptions: {ignoreSearch: true}
|
cacheQueryOptions: {ignoreSearch: true, ignoreVary: true}
|
||||||
}],
|
}],
|
||||||
dataGroups: [{
|
dataGroups: [{
|
||||||
name: 'other',
|
name: 'other',
|
||||||
|
@ -241,7 +241,7 @@ describe('Generator', () => {
|
||||||
maxAge: 259200000,
|
maxAge: 259200000,
|
||||||
timeoutMs: 60000,
|
timeoutMs: 60000,
|
||||||
version: 1,
|
version: 1,
|
||||||
cacheQueryOptions: {ignoreSearch: false}
|
cacheQueryOptions: {ignoreSearch: false, ignoreVary: true}
|
||||||
}],
|
}],
|
||||||
navigationUrls: [
|
navigationUrls: [
|
||||||
{positive: true, regex: '^\\/.*$'},
|
{positive: true, regex: '^\\/.*$'},
|
||||||
|
|
|
@ -44,6 +44,7 @@ const manifest: Manifest = {
|
||||||
updateMode: 'prefetch',
|
updateMode: 'prefetch',
|
||||||
urls: ['/only.txt'],
|
urls: ['/only.txt'],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
}],
|
}],
|
||||||
navigationUrls: [],
|
navigationUrls: [],
|
||||||
hashTable: tmpHashTableForFs(dist),
|
hashTable: tmpHashTableForFs(dist),
|
||||||
|
@ -60,6 +61,7 @@ const manifestUpdate: Manifest = {
|
||||||
updateMode: 'prefetch',
|
updateMode: 'prefetch',
|
||||||
urls: ['/only.txt'],
|
urls: ['/only.txt'],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
}],
|
}],
|
||||||
navigationUrls: [],
|
navigationUrls: [],
|
||||||
hashTable: tmpHashTableForFs(distUpdate),
|
hashTable: tmpHashTableForFs(distUpdate),
|
||||||
|
|
|
@ -56,6 +56,7 @@ const manifest: Manifest = {
|
||||||
'/bar.txt',
|
'/bar.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataGroups: [
|
dataGroups: [
|
||||||
|
@ -67,7 +68,7 @@ const manifest: Manifest = {
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
maxAge: 5000,
|
maxAge: 5000,
|
||||||
version: 1,
|
version: 1,
|
||||||
cacheQueryOptions: {ignoreSearch: true},
|
cacheQueryOptions: {ignoreVary: true, ignoreSearch: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'testRefresh',
|
name: 'testRefresh',
|
||||||
|
@ -78,6 +79,7 @@ const manifest: Manifest = {
|
||||||
refreshAheadMs: 1000,
|
refreshAheadMs: 1000,
|
||||||
maxAge: 5000,
|
maxAge: 5000,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'testFresh',
|
name: 'testFresh',
|
||||||
|
@ -87,6 +89,7 @@ const manifest: Manifest = {
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
maxAge: 5000,
|
maxAge: 5000,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
navigationUrls: [],
|
navigationUrls: [],
|
||||||
|
@ -201,7 +204,8 @@ describe('data cache', () => {
|
||||||
await makeRequest(scope, '/api/a');
|
await makeRequest(scope, '/api/a');
|
||||||
// the second one will be loaded from the cache
|
// the second one will be loaded from the cache
|
||||||
await makeRequest(scope, '/api/a');
|
await makeRequest(scope, '/api/a');
|
||||||
expect(matchSpy).toHaveBeenCalledWith(new MockRequest('/api/a'), {ignoreSearch: true});
|
expect(matchSpy).toHaveBeenCalledWith(
|
||||||
|
new MockRequest('/api/a'), {ignoreVary: true, ignoreSearch: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('still matches if search differs but ignoreSearch is enabled', async () => {
|
it('still matches if search differs but ignoreSearch is enabled', async () => {
|
||||||
|
@ -312,6 +316,27 @@ describe('data cache', () => {
|
||||||
|
|
||||||
expect(await res2).toBe('');
|
expect(await res2).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('CacheQueryOptions are passed through when falling back to cache', async () => {
|
||||||
|
const matchSpy = spyOn(MockCache.prototype, 'match').and.callThrough();
|
||||||
|
await makeRequest(scope, '/fresh/data');
|
||||||
|
server.clearRequests();
|
||||||
|
scope.updateServerState(serverUpdate);
|
||||||
|
serverUpdate.pause();
|
||||||
|
const [res, done] = makePendingRequest(scope, '/fresh/data');
|
||||||
|
|
||||||
|
await serverUpdate.nextRequest;
|
||||||
|
|
||||||
|
// Since the network request doesn't return within the timeout of 1,000ms,
|
||||||
|
// this should return cached data.
|
||||||
|
scope.advance(2000);
|
||||||
|
await res;
|
||||||
|
expect(matchSpy).toHaveBeenCalledWith(new MockRequest('/fresh/data'), {ignoreVary: true});
|
||||||
|
|
||||||
|
// Unpausing allows the worker to continue with caching.
|
||||||
|
serverUpdate.unpause();
|
||||||
|
await done;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -70,6 +70,7 @@ const brokenManifest: Manifest = {
|
||||||
'/foo.txt',
|
'/foo.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
}],
|
}],
|
||||||
dataGroups: [],
|
dataGroups: [],
|
||||||
navigationUrls: processNavigationUrls(''),
|
navigationUrls: processNavigationUrls(''),
|
||||||
|
@ -89,6 +90,7 @@ const brokenLazyManifest: Manifest = {
|
||||||
'/foo.txt',
|
'/foo.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lazy-assets',
|
name: 'lazy-assets',
|
||||||
|
@ -98,6 +100,7 @@ const brokenLazyManifest: Manifest = {
|
||||||
'/bar.txt',
|
'/bar.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataGroups: [],
|
dataGroups: [],
|
||||||
|
@ -143,6 +146,7 @@ const manifest: Manifest = {
|
||||||
patterns: [
|
patterns: [
|
||||||
'/unhashed/.*',
|
'/unhashed/.*',
|
||||||
],
|
],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'other',
|
name: 'other',
|
||||||
|
@ -153,6 +157,7 @@ const manifest: Manifest = {
|
||||||
'/qux.txt',
|
'/qux.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lazy_prefetch',
|
name: 'lazy_prefetch',
|
||||||
|
@ -165,6 +170,7 @@ const manifest: Manifest = {
|
||||||
'/lazy/unchanged2.txt',
|
'/lazy/unchanged2.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
dataGroups: [
|
dataGroups: [
|
||||||
|
@ -177,6 +183,7 @@ const manifest: Manifest = {
|
||||||
patterns: [
|
patterns: [
|
||||||
'/api/.*',
|
'/api/.*',
|
||||||
],
|
],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'api-static',
|
name: 'api-static',
|
||||||
|
@ -187,6 +194,7 @@ const manifest: Manifest = {
|
||||||
patterns: [
|
patterns: [
|
||||||
'/api-static/.*',
|
'/api-static/.*',
|
||||||
],
|
],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
navigationUrls: processNavigationUrls(''),
|
navigationUrls: processNavigationUrls(''),
|
||||||
|
@ -213,6 +221,7 @@ const manifestUpdate: Manifest = {
|
||||||
patterns: [
|
patterns: [
|
||||||
'/unhashed/.*',
|
'/unhashed/.*',
|
||||||
],
|
],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'other',
|
name: 'other',
|
||||||
|
@ -223,6 +232,7 @@ const manifestUpdate: Manifest = {
|
||||||
'/qux.txt',
|
'/qux.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lazy_prefetch',
|
name: 'lazy_prefetch',
|
||||||
|
@ -235,6 +245,7 @@ const manifestUpdate: Manifest = {
|
||||||
'/lazy/unchanged2.txt',
|
'/lazy/unchanged2.txt',
|
||||||
],
|
],
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navigationUrls: processNavigationUrls(
|
navigationUrls: processNavigationUrls(
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
import {PrefetchAssetGroup} from '../src/assets';
|
import {PrefetchAssetGroup} from '../src/assets';
|
||||||
import {CacheDatabase} from '../src/db-cache';
|
import {CacheDatabase} from '../src/db-cache';
|
||||||
import {IdleScheduler} from '../src/idle';
|
import {IdleScheduler} from '../src/idle';
|
||||||
|
import {MockCache} from '../testing/cache';
|
||||||
|
import {MockRequest} from '../testing/fetch';
|
||||||
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTable, tmpManifestSingleAssetGroup} from '../testing/mock';
|
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTable, tmpManifestSingleAssetGroup} from '../testing/mock';
|
||||||
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
||||||
|
|
||||||
|
@ -19,7 +21,7 @@ if (!SwTestHarness.envIsSupported()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const dist = new MockFileSystemBuilder()
|
const dist = new MockFileSystemBuilder()
|
||||||
.addFile('/foo.txt', 'this is foo')
|
.addFile('/foo.txt', 'this is foo', {Vary: 'Accept'})
|
||||||
.addFile('/bar.txt', 'this is bar')
|
.addFile('/bar.txt', 'this is bar')
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -84,6 +86,12 @@ describe('prefetch assets', () => {
|
||||||
const err = await errorFrom(group.initializeFully());
|
const err = await errorFrom(group.initializeFully());
|
||||||
expect(err.message).toContain('Hash mismatch');
|
expect(err.message).toContain('Hash mismatch');
|
||||||
});
|
});
|
||||||
|
it('CacheQueryOptions are passed through', async () => {
|
||||||
|
await group.initializeFully();
|
||||||
|
const matchSpy = spyOn(MockCache.prototype, 'match').and.callThrough();
|
||||||
|
await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||||
|
expect(matchSpy).toHaveBeenCalledWith(new MockRequest('/foo.txt'), {ignoreVary: true});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -225,6 +225,7 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
|
||||||
updateMode: 'prefetch',
|
updateMode: 'prefetch',
|
||||||
urls: files,
|
urls: files,
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
cacheQueryOptions: {ignoreVary: true}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
navigationUrls: [],
|
navigationUrls: [],
|
||||||
|
|
Loading…
Reference in New Issue