Skip to content

Commit 281e3b3

Browse files
committed
fix(service-worker): include headers in requests for assets
Previously, when requesting non-cached asset resources from the network, the ServiceWorker would strip off all request metadata (including headers). This was done in order to avoid issues with opaque responses, but it turned out to be overly aggressive, breaking/worsening legit usecases (such as requesting compressed data). This commit fixes this by preserving the headers of such requests. For reference, Workbox passes the original request as is. (See for example the [NetworkFirst][1] strategy). > **Note** > Data requests (i.e. requests for URLs that belong to a data-group) are not affected by this. They already use the original resource as is. [1]: https://github.com/GoogleChrome/workbox/blob/95f97a207fd51efb3f8a653f6e3e58224183a778/packages/workbox-strategies/src/NetworkFirst.ts#L90 Fixes #24227
1 parent 36351b3 commit 281e3b3

File tree

4 files changed

+96
-8
lines changed

4 files changed

+96
-8
lines changed

packages/service-worker/worker/src/app-version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export class AppVersion implements UpdateSource {
194194
}
195195

196196
// This was a navigation request. Re-enter `handleFetch` with a request for
197-
// the URL.
197+
// the index URL.
198198
return this.handleFetch(this.adapter.newRequest(this.indexUrl), event);
199199
}
200200

packages/service-worker/worker/src/assets.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,8 @@ export abstract class AssetGroup {
150150
}
151151
}
152152

153-
// No already-cached response exists, so attempt a fetch/cache operation. The original request
154-
// may specify things like credential inclusion, but for assets these are not honored in order
155-
// to avoid issues with opaque responses. The SW requests the data itself.
156-
const res = await this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
153+
// No already-cached response exists, so attempt a fetch/cache operation.
154+
const res = await this.fetchAndCacheOnce(this.newRequestWithMetadata(req.url, req));
157155

158156
// If this is successful, the response needs to be cloned as it might be used to respond to
159157
// multiple fetch operations at the same time.
@@ -360,7 +358,7 @@ export abstract class AssetGroup {
360358
}
361359

362360
// Unwrap the redirect directly.
363-
return this.fetchFromNetwork(this.adapter.newRequest(res.url), redirectLimit - 1);
361+
return this.fetchFromNetwork(this.newRequestWithMetadata(res.url, req), redirectLimit - 1);
364362
}
365363

366364
return res;
@@ -413,7 +411,7 @@ export abstract class AssetGroup {
413411
// data, or because the version on the server really doesn't match. A cache-busting
414412
// request will differentiate these two situations.
415413
// TODO: handle case where the URL has parameters already (unlikely for assets).
416-
const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
414+
const cacheBustReq = this.newRequestWithMetadata(this.cacheBust(req.url), req);
417415
response = await this.safeFetch(cacheBustReq);
418416

419417
// If the response was successful, check the contents against the canonical hash.
@@ -476,6 +474,24 @@ export abstract class AssetGroup {
476474
return false;
477475
}
478476

477+
/**
478+
* Create a new `Request` based on the specified URL and `RequestInit` options, preserving only
479+
* metadata that are known to be safe.
480+
*
481+
* Currently, only headers are preserved.
482+
*
483+
* NOTE:
484+
* Things like credential inclusion are intentionally omitted to avoid issues with opaque
485+
* responses.
486+
*
487+
* TODO(gkalpak):
488+
* Investigate preserving more metadata. See, also, discussion on preserving `mode`:
489+
* https://github.com/angular/angular/issues/41931#issuecomment-1227601347
490+
*/
491+
private newRequestWithMetadata(url: string, options: RequestInit): Request {
492+
return this.adapter.newRequest(url, {headers: options.headers});
493+
}
494+
479495
/**
480496
* Construct a cache-busting URL for a given URL.
481497
*/

packages/service-worker/worker/test/happy_spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const dist =
3535
.addFile('/redirect-target.txt', 'this was a redirect')
3636
.addFile('/lazy/unchanged1.txt', 'this is unchanged (1)')
3737
.addFile('/lazy/unchanged2.txt', 'this is unchanged (2)')
38+
.addFile('/lazy/redirect-target.txt', 'this was a redirect too')
3839
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed', {'Cache-Control': 'max-age=10'})
3940
.addUnhashedFile('/unhashed/b.txt', 'this is unhashed b', {'Cache-Control': 'no-cache'})
4041
.addUnhashedFile('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'})
@@ -161,6 +162,7 @@ const manifest: Manifest = {
161162
urls: [
162163
'/baz.txt',
163164
'/qux.txt',
165+
'/lazy/redirected.txt',
164166
],
165167
patterns: [],
166168
cacheQueryOptions: {ignoreVary: true},
@@ -270,6 +272,7 @@ const manifestUpdate: Manifest = {
270272
const serverBuilderBase = new MockServerStateBuilder()
271273
.withStaticFiles(dist)
272274
.withRedirect('/redirected.txt', '/redirect-target.txt')
275+
.withRedirect('/lazy/redirected.txt', '/lazy/redirect-target.txt')
273276
.withError('/error.txt');
274277

275278
const server = serverBuilderBase.withManifest(manifest).build();
@@ -1578,6 +1581,71 @@ describe('Driver', () => {
15781581
});
15791582
});
15801583

1584+
describe('request metadata', () => {
1585+
it('passes headers through to the server', async () => {
1586+
// Request a lazy-cached asset (so that it is fetched from the network) and provide headers.
1587+
const reqInit = {
1588+
headers: {SomeHeader: 'SomeValue'},
1589+
};
1590+
expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz');
1591+
1592+
// Verify that the headers were passed through to the network.
1593+
const [bazReq] = server.getRequestsFor('/baz.txt');
1594+
expect(bazReq.headers.get('SomeHeader')).toBe('SomeValue');
1595+
});
1596+
1597+
it('does not pass non-allowed metadata through to the server', async () => {
1598+
// Request a lazy-cached asset (so that it is fetched from the network) and provide some
1599+
// metadata.
1600+
const reqInit = {
1601+
credentials: 'include',
1602+
mode: 'same-origin',
1603+
unknownOption: 'UNKNOWN',
1604+
};
1605+
expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz');
1606+
1607+
// Verify that the metadata were not passed through to the network.
1608+
const [bazReq] = server.getRequestsFor('/baz.txt');
1609+
expect(bazReq.credentials).toBe('same-origin'); // The default value.
1610+
expect(bazReq.mode).toBe('cors'); // The default value.
1611+
expect((bazReq as any).unknownOption).toBeUndefined();
1612+
});
1613+
1614+
describe('for redirect requests', () => {
1615+
it('passes headers through to the server', async () => {
1616+
// Request a redirected, lazy-cached asset (so that it is fetched from the network) and
1617+
// provide headers.
1618+
const reqInit = {
1619+
headers: {SomeHeader: 'SomeValue'},
1620+
};
1621+
expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit))
1622+
.toBe('this was a redirect too');
1623+
1624+
// Verify that the headers were passed through to the network.
1625+
const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
1626+
expect(redirectReq.headers.get('SomeHeader')).toBe('SomeValue');
1627+
});
1628+
1629+
it('does not pass non-allowed metadata through to the server', async () => {
1630+
// Request a redirected, lazy-cached asset (so that it is fetched from the network) and
1631+
// provide some metadata.
1632+
const reqInit = {
1633+
credentials: 'include',
1634+
mode: 'same-origin',
1635+
unknownOption: 'UNKNOWN',
1636+
};
1637+
expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit))
1638+
.toBe('this was a redirect too');
1639+
1640+
// Verify that the metadata were not passed through to the network.
1641+
const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
1642+
expect(redirectReq.credentials).toBe('same-origin'); // The default value.
1643+
expect(redirectReq.mode).toBe('cors'); // The default value.
1644+
expect((redirectReq as any).unknownOption).toBeUndefined();
1645+
});
1646+
});
1647+
});
1648+
15811649
describe('unhashed requests', () => {
15821650
beforeEach(async () => {
15831651
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');

packages/service-worker/worker/testing/mock.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ export class MockServerState {
179179
this.resolve = null;
180180
}
181181

182+
getRequestsFor(url: string): Request[] {
183+
return this.requests.filter(req => req.url.split('?')[0] === url);
184+
}
185+
182186
assertSawRequestFor(url: string): void {
183187
if (!this.sawRequestFor(url)) {
184188
throw new Error(`Expected request for ${url}, got none.`);
@@ -192,7 +196,7 @@ export class MockServerState {
192196
}
193197

194198
sawRequestFor(url: string): boolean {
195-
const matching = this.requests.filter(req => req.url.split('?')[0] === url);
199+
const matching = this.getRequestsFor(url);
196200
if (matching.length > 0) {
197201
this.requests = this.requests.filter(req => req !== matching[0]);
198202
return true;

0 commit comments

Comments
 (0)