From 6200732e2360690532686c131add006cd29f3b61 Mon Sep 17 00:00:00 2001 From: Peter Johan Salomonsen Date: Thu, 25 Apr 2019 22:51:10 +0300 Subject: [PATCH] feat(service-worker): support bypassing SW with specific header/query param (#30010) Add support for bypassing the ServiceWorker for a request by using the ngsw-bypass header or query parameter. Fixes #21191 PR Close #30010 --- aio/content/guide/service-worker-devops.md | 9 +++ packages/service-worker/worker/src/adapter.ts | 4 +- packages/service-worker/worker/src/driver.ts | 4 ++ .../service-worker/worker/test/happy_spec.ts | 68 +++++++++++++++++++ .../service-worker/worker/testing/fetch.ts | 10 +-- .../service-worker/worker/testing/scope.ts | 3 +- 6 files changed, 90 insertions(+), 8 deletions(-) diff --git a/aio/content/guide/service-worker-devops.md b/aio/content/guide/service-worker-devops.md index 3a6885fcf7..54db8d9855 100644 --- a/aio/content/guide/service-worker-devops.md +++ b/aio/content/guide/service-worker-devops.md @@ -147,6 +147,15 @@ normally. However, occasionally a bugfix or feature in the Angular service worker requires the invalidation of old caches. In this case, the app will be refreshed transparently from the network. +### Bypassing the service worker + +In some cases, you may want to bypass the service worker entirely and let the browser handle the +request instead. An example is when you rely on a feature that is currently not supported in service +workers (e.g. +[reporting progress on uploaded files](https://github.com/w3c/ServiceWorker/issues/1141)). + +To bypass the service worker you can set `ngsw-bypass` as a request header, or as a query parameter. +(The value of the header or query parameter is ignored and can be empty or omitted.) ## Debugging the Angular service worker diff --git a/packages/service-worker/worker/src/adapter.ts b/packages/service-worker/worker/src/adapter.ts index a8fb54058c..b9d81091f5 100644 --- a/packages/service-worker/worker/src/adapter.ts +++ b/packages/service-worker/worker/src/adapter.ts @@ -52,9 +52,9 @@ export class Adapter { /** * Extract the pathname of a URL. */ - parseUrl(url: string, relativeTo?: string): {origin: string, path: string} { + parseUrl(url: string, relativeTo?: string): {origin: string, path: string, search: string} { const parsed = new URL(url, relativeTo); - return {origin: parsed.origin, path: parsed.pathname}; + return {origin: parsed.origin, path: parsed.pathname, search: parsed.search}; } /** diff --git a/packages/service-worker/worker/src/driver.ts b/packages/service-worker/worker/src/driver.ts index d7b69ba57f..36337f072b 100644 --- a/packages/service-worker/worker/src/driver.ts +++ b/packages/service-worker/worker/src/driver.ts @@ -179,6 +179,10 @@ export class Driver implements Debuggable, UpdateSource { const scopeUrl = this.scope.registration.scope; const requestUrlObj = this.adapter.parseUrl(req.url, scopeUrl); + if (req.headers.has('ngsw-bypass') || /[?&]ngsw-bypass(?:[=&]|$)/i.test(requestUrlObj.search)) { + return; + } + // The only thing that is served unconditionally is the debug page. if (requestUrlObj.path === '/ngsw/state') { // Allow the debugger to handle the request, but don't affect SW state in any other way. diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index ec431995a9..79c54d637d 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -711,6 +711,74 @@ import {async_beforeEach, async_fit, async_it} from './async'; serverUpdate.assertNoOtherRequests(); }); + async_it('should bypass serviceworker on ngsw-bypass parameter', async() => { + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypasss': 'anything'}}); + server.assertSawRequestFor('/foo.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar.txt?ngsw-bypass=true'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?ngsw-bypasss=true'); + server.assertSawRequestFor('/bar.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar.txt?ngsw-bypaSS=something'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?testparam=test&ngsw-byPASS=anything'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?testparam=test&angsw-byPASS=anything'); + server.assertSawRequestFor('/bar.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar&ngsw-bypass=true.txt?testparam=test&angsw-byPASS=anything'); + server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar&ngsw-bypass=true.txt'); + server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); + + server.clearRequests(); + + await makeRequest( + scope, '/bar&ngsw-bypass=true.txt?testparam=test&ngSW-BYPASS=SOMETHING&testparam2=test'); + server.assertNoRequestFor('/bar&ngsw-bypass=true.txt'); + + await makeRequest(scope, '/bar?testparam=test&ngsw-bypass'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?testparam=test&ngsw-bypass&testparam2'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-bypass&testparam2'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-bypass=&foo=ngsw-bypass'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-byapass&testparam2'); + server.assertSawRequestFor('/bar'); + + }); + async_it('unregisters when manifest 404s', async() => { expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); await driver.initialized; diff --git a/packages/service-worker/worker/testing/fetch.ts b/packages/service-worker/worker/testing/fetch.ts index 6545168012..acd1ba91d1 100644 --- a/packages/service-worker/worker/testing/fetch.ts +++ b/packages/service-worker/worker/testing/fetch.ts @@ -61,21 +61,21 @@ export class MockHeaders implements Headers { [Symbol.iterator]() { return this.map[Symbol.iterator](); } - append(name: string, value: string): void { this.map.set(name, value); } + append(name: string, value: string): void { this.map.set(name.toLowerCase(), value); } - delete (name: string): void { this.map.delete(name); } + delete (name: string): void { this.map.delete(name.toLowerCase()); } entries() { return this.map.entries(); } forEach(callback: Function): void { this.map.forEach(callback as any); } - get(name: string): string|null { return this.map.get(name) || null; } + get(name: string): string|null { return this.map.get(name.toLowerCase()) || null; } - has(name: string): boolean { return this.map.has(name); } + has(name: string): boolean { return this.map.has(name.toLowerCase()); } keys() { return this.map.keys(); } - set(name: string, value: string): void { this.map.set(name, value); } + set(name: string, value: string): void { this.map.set(name.toLowerCase(), value); } values() { return this.map.values(); } } diff --git a/packages/service-worker/worker/testing/scope.ts b/packages/service-worker/worker/testing/scope.ts index 5ce782c98a..997b7615cb 100644 --- a/packages/service-worker/worker/testing/scope.ts +++ b/packages/service-worker/worker/testing/scope.ts @@ -184,7 +184,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context }, new MockHeaders()); } - parseUrl(url: string, relativeTo?: string): {origin: string, path: string} { + parseUrl(url: string, relativeTo?: string): {origin: string, path: string, search: string} { const parsedUrl: URL = (typeof URL === 'function') ? new URL(url, relativeTo) : require('url').parse(require('url').resolve(relativeTo || '', url)); @@ -192,6 +192,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context return { origin: parsedUrl.origin || `${parsedUrl.protocol}//${parsedUrl.host}`, path: parsedUrl.pathname, + search: parsedUrl.search || '', }; }