Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

There should be a seamless way to get rid of broken service worker state #43163

Closed
naveedahmed1 opened this issue Aug 15, 2021 · 16 comments
Closed
Labels
area: service-worker Issues related to the @angular/service-worker package
Milestone

Comments

@naveedahmed1
Copy link
Contributor

Which @angular/* package(s) are the source of the bug?

service-worker

Is this a regression?

No

Description

I faced a strange issue with Angular Service worker. My website users were being served with old version of the app which was never getting updated.

If I close browser window, open again and navigate to https://www.mustakbil.com it serves the cached version of the app from service worker. If I refresh the page it loads new version of the app but when I close the browser and reopen it shows same old version of the app.

When I debug the service worker, it shows Driver state: EXISTING_CLIENTS_ONLY. Here's the output of /ngsw/state

NGSW Debug Info:

Driver version: 12.2.0-rc.0
Driver state: EXISTING_CLIENTS_ONLY (Degraded due to failed initialization: Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting)
Error: Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting)
    at PrefetchAssetGroup.<anonymous> (https://www.mustakbil.com/combined-sw.js:1:9886)
    at Generator.next (<anonymous>)
    at fulfilled (https://www.mustakbil.com/combined-sw.js:1:175))
Latest manifest hash: 226aa816c81da75f68299ffb568a41bd7f881589
Last update check: 2s922u

=== Version 226aa816c81da75f68299ffb568a41bd7f881589 ===

Clients: 1c822e1d-9199-41f9-ad57-3cda8a91bfd9

=== Idle Task Queue ===
Last update tick: 8s736u
Last update run: 3s730u
Task queue:


Debug log:

[17s149u] Error(Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting), Error: Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting)
    at PrefetchAssetGroup.<anonymous> (https://www.mustakbil.com/combined-sw.js:1:9886)
    at Generator.next (<anonymous>)
    at fulfilled (https://www.mustakbil.com/combined-sw.js:1:175)) Error occurred while updating to manifest 7bc1992cebed8206fd5798aaeae287c4b5038c1f
[1s976u] Error(Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting), Error: Hash mismatch (cacheBustedFetchFromNetwork): https://www.mustakbil.com/styles.dbc7cf3b891664628fe9.css: expected 02817d65d4e9df85dab3d21324d5ec2c105fa165, got 3aa6510e4f778214e6e6958b7bcea9398baf9d8b (after cache busting)
    at PrefetchAssetGroup.<anonymous> (https://www.mustakbil.com/combined-sw.js:1:9886)
    at Generator.next (<anonymous>)
    at fulfilled (https://www.mustakbil.com/combined-sw.js:1:175)) Error occurred while updating to manifest 7bc1992cebed8206fd5798aaeae287c4b5038c1f

I tried the approach of safety worker as suggested in Angular docs (https://angular.io/guide/service-worker-devops#safety-worker) and got rid of my old worker.

Docs suggest For most sites, this means that you should serve the safety worker at the old Service Worker URL forever., this means we should rename our main worker. I did the same. Now when I deployed my app, I noticed that safety worker always kicks in; and my new service worker doesn't get registered as a result I see below error in my console log:

NGSW Safety Worker - unregistered old service worker
main.a91480950c35a82b4c64.js:1 ERROR DOMException: Registration failed - storage error

So apparently its now stuck in safety worker.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

NGSW Safety Worker - unregistered old service worker
main.a91480950c35a82b4c64.js:1 ERROR DOMException: Registration failed - storage error


### Please provide the environment you discovered this bug in

```true
Angular CLI: 13.0.0-next.0
Node: 14.16.0
Package Manager: npm 6.14.11
OS: win32 x64

Angular: 13.0.0-next.1
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... platform-server, router, service-worker

Package                          Version
----------------------------------------------------------
@angular-devkit/architect        0.1201.1
@angular-devkit/build-angular    13.0.0-next.0
@angular-devkit/core             13.0.0-next.0
@angular-devkit/schematics       13.0.0-next.0
@angular/cdk                     13.0.0-next.0
@angular/cli                     13.0.0-next.0
@angular/fire                    6.1.5
@angular/google-maps             13.0.0-next.0
@angular/material                13.0.0-next.0
@angular/pwa                     13.0.0-next.0
@nguniversal/aspnetcore-engine   12.1.0
@nguniversal/builders            12.1.0
@nguniversal/common              12.1.0
@schematics/angular              13.0.0-next.0
rxjs                             6.6.7
typescript                       4.3.5

Anything else?

No response

@naveedahmed1
Copy link
Contributor Author

naveedahmed1 commented Aug 15, 2021

Update

It seems that even safety worker approach doesn't unless I cleared sw cache:

self.addEventListener('install', event => {
  self.skipWaiting();
});

self.addEventListener('activate', event => {
  event.waitUntil(self.clients.claim());
  self.registration.unregister().then(() => {
      console.log('NGSW Safety Worker - unregistered old service worker');
      console.log('clearing cache now');

      caches.keys().then(function (cacheNames) {
          cacheNames.forEach(function (cacheName) {
              caches.delete(cacheName);
          });
      });
  });
});

And as docs suggest to keep safety worker live, if I do so, I see below error:

main.a91480950c35a82b4c64.js:1 ERROR DOMException: Registration failed - storage error

@dylhunn dylhunn added the area: service-worker Issues related to the @angular/service-worker package label Aug 16, 2021
@ngbot ngbot bot added this to the needsTriage milestone Aug 16, 2021
@naveedahmed1
Copy link
Contributor Author

On further investigation I found that since we are also using FirebaseMessaging it automatically registers service worker with filename firebase-messaging-sw.js. And to make FirebaseMessaging worker work with Angular Worker, we combined both worker in a file with name firebase-messaging-sw.js.

Now since our service worker was in error state and we wanted to get rid of that worker, we replaced the contents of firebase-messaging-sw.js with Angular safety worker, but Firebase was still looking for file firebase-messaging-sw.js and registering that as worker, which ultimately was unregistering any registered service worker. As a result even our new service worker was getting unregistered and SwUpdate.checkForUpdate(); was throwing error:

main.a91480950c35a82b4c64.js:1 ERROR DOMException: Registration failed - storage error

I got this fixed by passing my Angular Service Worker (which is now in a new file with name firebase-messaging-sw-v1.js and registered through ServiceWorkerModule.register) to AngularFireMessaging
.
Below is my code:

constructor(
....
private messaging: AngularFireMessaging,
....
  ){
navigator.serviceWorker.getRegistration().then((registration) => {
      messaging.useServiceWorker(registration);
    });
}

@gkalpak
Copy link
Member

gkalpak commented Aug 17, 2021

Thx for the update, @naveedahmed1!

I am going to close this, since I don't think there is anything that can be done on our side (ServiceWorkers have some limitations/idiosyncrasies and we have to live with them 😃).

AFAICT, the methods described in https://angular.io/guide/service-worker-devops should work (if applied correctly).
Feel free to suggest any docs improvements that would make the instructions more clear. Also, if you have ideas on better methods for dealing with a broken SW, I'm all ears 😃

@gkalpak gkalpak closed this as completed Aug 17, 2021
@naveedahmed1
Copy link
Contributor Author

@gkalpak it seems that the safety worker approach doesn't work unless the user refreshed the page.

Some of our clients are reporting they are seeing below error in console::

Failed to load ‘https://www.mustakbil.com/2303.0a2794b058278d893bcb.js’. A ServiceWorker intercepted the request and encountered an unexpected error.

Refreshing the page fixes the issue.

Is there any way to prompt user to refresh page if the service worker is broken?

@gkalpak
Copy link
Member

gkalpak commented Sep 1, 2021

It seems that the safety worker approach doesn't work unless the user refreshed the page.

Yes, that's expected. There is no way to replace an activated ServiceWorker unless a new ServiceWorker script is downloaded and that only happens a new navigations.

Is there any way to prompt user to refresh page if the service worker is broken?

The Angular SW does (partially or completely) deactivate itself if it detects that it is in a broken state. But if you provide your own SW script (for example, by concatenating the Angular SW with a different one), how would you detect that the SW is broken?

@naveedahmed1
Copy link
Contributor Author

What if the page is reloaded automatically after service worker detects that its in broken state?

When the service worker is in a broken state, and the JavaScript bundle needed to render that page is missing. In that case what user sees is a page with just top navigation bar and empty body contents. Even clicking the navigation links doesn't work. In that case I believe its more useful to automatically refresh that page bypassing service worker (or after unregistering sw).

@gkalpak
Copy link
Member

gkalpak commented Sep 1, 2021

Theoretically, that shouldn't happen with the Angular SW, because when it detects that it is broken it will forward requests to the network. If it is an unrecoverable state, it will also emit on SwUpdate#unrecoverable (so the app could do the reload).

Automatically reloading could be a bad UX (for example, it could cause the user to lose work).
I know it sounds good in theory, but in practice there is no reliable way to detect when the SW is broken in a way that a reload is the best approach.

@naveedahmed1
Copy link
Contributor Author

I see, I recently added below code to my ServiceWorkerUpdateService

this.updates.unrecoverable.subscribe(event => {
        console.log('sw is in unrecoverable state clearing ngsw cache:', event.reason);

        //clear service worker cache
        this.clearSWCacheStorage().then(() => {

          //remove any existing service worker registrations
          if ('serviceWorker' in navigator) {
            navigator.serviceWorker.getRegistrations().then(function (registrations) {
              //returns installed service workers
              if (registrations.length) {
                for (let registration of registrations) {
                  registration.unregister();
                }
              }

              //notify user and reload page
              this.snackBar.open('New version available', 'UPDATE NOW!', 'success', false, null, true);
              this.snackBarOnActionSub = this.snackBar.onAction.subscribe(() => {
                if (this.snackBarOnActionSub) {
                  this.snackBarOnActionSub.unsubscribe();
                }

                //reload page
                window.location.reload();
              });

            });
          }

        });

      });


Is it the correct way to handle such situations?

In my case, I removed the old js bundles from the server, which is causing the issue for the users.

I added above code in latest version, and I hope it will help avoiding such situations in future.

I have also replaced the contents of my old service worker with contents of safety worker.

Is there anything else I should do?

@gkalpak
Copy link
Member

gkalpak commented Sep 1, 2021

Regarding the unrecoverable event-handler:

  • It shouldn't be necessary to clear all caches (since it could be that it is just a specific version that is problematic, while the rest are fine). The SW should be able to clean the bad caches itself.
  • The 'serviceWorker' in navigator is redundant, since it will always be true if SwUpdate#unrecoverable emits.

Other than that, it looks fine.

@naveedahmed1
Copy link
Contributor Author

Thanks for your feedback @gkalpak, really appreciate.

Here's my clear cache code, I am clearing just Angular Service Worker cache:

clearSWCacheStorage(): Promise<Promise<boolean>[][]> {
    return caches.keys().then(cacheKeys =>
      Promise.all(
        cacheKeys.map(cacheKey => {
          const ngswRegex = /^(ngsw).*/;
          if (ngswRegex.test(cacheKey)) {
            return caches
              .open(cacheKey)
              .then(cache => cache.keys().then(requests => requests.map(req => cache.delete(req))));
          }
        })
      )
    );
  }

What you are suggesting is, we don't need to clear SW cache as well, SW should be able to handle that automatically, right?

@gkalpak
Copy link
Member

gkalpak commented Sep 1, 2021

Yes, you shouldn't need to clear all SW caches.

The SW may be serving multiple app versions simultaneously (i.e. serving different versions to different tabs). If a single version is broken, you don't need to delete all caches (since some may correspond to good versions). The SW will stop using the caches corresponding to the broken version (and will eventually remove them, since they are unused).

@naveedahmed1
Copy link
Contributor Author

Thank you so much once again @gkalpak :)

@naveedahmed1
Copy link
Contributor Author

@gkalpak nothing worked until we updated the safety worker to the follow:

self.addEventListener('install', event => {
    self.skipWaiting();
});

self.addEventListener('activate', event => {
    event.waitUntil(self.clients.claim());
    self.registration.unregister().then(() => {
        console.log('NGSW Safety Worker - unregistered old service worker');
    });
});


caches.keys().then(cacheKeys =>
    Promise.all(
        cacheKeys.map(cacheKey => {
            const ngswRegex = /^(ngsw).*/;
            if (ngswRegex.test(cacheKey)) {
                return caches
                    .open(cacheKey)
                    .then(cache => cache.keys().then(requests => requests.map(req => cache.delete(req))));
            }
        })
    )
);

Before that, new service worker was installed successfully but it was randomly showing contents from old cache.

For example our service worker caches home page https://www.mustakbil.com/

  1. We had a broken sw (firebase-messaging-sw-v12.js), to get rid of that worker we replaced contents of our service worker (firebase-messaging-sw-v12.js) with contents of default safety worker.
  2. Deployed app with new worker (firebase-messaging-sw-v13.js).
  3. Reloaded the page https://www.mustakbil.com/
  4. Verified from console that the active worker is latest sw i.e. firebase-messaging-sw-v13.js
  5. Refreshed the page again, and checked console, it was showing error with text like "Service worker intercepted the request https://www.mustakbil.com/main.xxxxxxx.js but encountered an error." and app was broken.
  6. I checked the Network tab and found that the request for (https://www.mustakbil.com/) was served from an old cache of old service worker.

The question is why new service worker was serving the request from the cache of old service worker.

@gkalpak
Copy link
Member

gkalpak commented Sep 2, 2021

Hm...yeah, it makes sense to have the safety worker remove the ngsw caches. Feel free to submit a PR to augment the safety worker with that functionality.

@naveedahmed1
Copy link
Contributor Author

@gkalpak just created my first ever pull request in an open source repo, lets see how it goes :D

Here's the link to the pull request #43324

AndrewKushnir pushed a commit that referenced this issue Sep 13, 2021
)

clear angular service worker cache in safety worker to ensure stale
or broken contents are not served in future requests

Fixes #43163

PR Close #43324
AndrewKushnir pushed a commit that referenced this issue Sep 13, 2021
)

clear angular service worker cache in safety worker to ensure stale
or broken contents are not served in future requests

Fixes #43163

PR Close #43324
TeriGlover pushed a commit to TeriGlover/angular that referenced this issue Sep 16, 2021
…ular#43324)

clear angular service worker cache in safety worker to ensure stale
or broken contents are not served in future requests

Fixes angular#43163

PR Close angular#43324
TeriGlover pushed a commit to TeriGlover/angular that referenced this issue Sep 22, 2021
…ular#43324)

clear angular service worker cache in safety worker to ensure stale
or broken contents are not served in future requests

Fixes angular#43163

PR Close angular#43324
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Oct 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: service-worker Issues related to the @angular/service-worker package
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants