Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'Unexpected token <' on every new build of angular production PWA until site refresh

I know there are some similar questions like so unexpected token after angular 2 production build but it doesnt actually answer my question.

Basically I have an angular app and its a PWA, now everytime I do a new production build the first time I visit the site I get an error like so

enter image description here

and then as soon as I refresh the page, the site works as normal.

This happens on any browser/device

Now my suspicion is that every time the app is updated, the main bundled js file changes, and the PWA has cached the old one and the app is still trying to use the new one.

My Project structure is as follows

I have a root module that lazy loads the other modules now the first module that gets loaded is my account module so the user can log in

this is my root-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

const routes: Routes = [
    { path: '', redirectTo: '/account/login', pathMatch: 'full' },
    {
        path: 'account',
        loadChildren: 'account/account.module#AccountModule',
        data: { preload: true }
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
    exports: [RouterModule],
    providers: []
})
export class RootRoutingModule {

}

so technically the first module the user gets to is the account module, but obviously it has to be redirected from the root module.

So what I did was in my root component module I check to see if the Service Worker needs updating like so...

root.component.ts

@import { SwUpdate } from '@angular/service-worker'
...

export class...

constructor(
    private _sw: SwUpdate
) {
    if (this._sw.isEnabled) {
        this._sw.available
            .subscribe(() => {
                this._sw.activateUpdate()
                    .then(() => {
                        window.location.reload(true);
                    });
            });
    }
}     

now this should check to see if there is an update and then just refresh the page.. but it doesn't work.

this is my ngsw-config.json

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "App",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js",
          "!/main*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

as you can see I am excluding the main.js chunk file which should eliminate this issue... but it doesn't

Now when I check the network tab after a new build this is what I get

enter image description here This image was before I excluded the main.js file

this is what my main.js calls look like after I have excluded it from the ngsw.config

enter image description here

I then thought to just try and catch this error using my sentry error handler like so..

export class RavenErrorHandler implements ErrorHandler {
  handleError(err: any): void {
    console.log('error', err);
    if (err.toLowerCase().includes('token <')) {
        window.location.reload(true);
    } else {
        Raven.captureException(err);
    }
  }
}

but this isn't working, I am getting the error in sentry but the app is not reloading??

Now I have done some testing, If you visit the site after a fresh build on private mode you will never get the error, it only seems to happen if you have visited the site before, which is what makes me think its a caching issue

My Application is hosted on Azure and my application is using Angular 7.27

Any help would be appreciated!

like image 699
Smokey Dawson Avatar asked Mar 14 '19 05:03

Smokey Dawson


2 Answers

The issue is caused by the installed ServiceWorker, which is the one that interferes and caches all responses, including the ones that you have just updated.

More information about service workers: MDN - Using Service workers

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

That is why window.location.reload() does not work, because it does make a new request but this request is hijacked by the installed ServiceWorker and instead of letting the real request go through to the real server and fetch the real response (updated main.js and assets), it just returns back to your app a cached response, which is the old main.js and not the updated one and that is why it fails.

More specifically the ServiceWorker is caching the main.js and other assets, which are generated each time you change your code. Main.js when loaded in turn it lazy-loads (makes an http request to) other chunks of your application (e.g. the 5.xxxxx.js).

When you do make a change in your code, and even though all your chunks gets updated, including main.js and the 5.xxxxxx.js chunk, the browser is still running the older main.js. This older main.js has a reference to the older pair of 5.xxxxxx.js which no longer exists. At this point if you do a reload() programatically, the installed ServiceWorker responds with the older cached main.js which tries in turn to lazy-load the older version of 5.xxxxx.js from the server which does not exist and therefore gets a response of an 404 html error. Hence the '<' token exception (it's actually the first character of the tag of the 404 error page).

This is evident from the network panel screenshot. If you examine the screenshot you will see that the main.js and all other assets are delivered from (from service worker), which means that those are the cached older versions and are different from the actual updated files that now exist in your web server.

To fix this issue, the ServiceWorker must be configured to not cache the main.js and instead always let this request to go through to the real server in order to fetch any updated version of main.js that in turn will lazy-load the updated 5.xxxx.js chunk that now actually exist in the web server.

Here is how you can configure the ngsw-config.json of ServiceWorker to specifically not cache the main.js file (assuming it is of the pattern main.xxxxxxx.js) in order to fix this issue:

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "App",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js",
          "/!main*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

run ng build --prod to rebuild your app and test it.

UPDATE:

To make sure that this is not a browser caching issue, pass true as the first argument to the window.location.reload() method. This instructs the reload to be executed without cached assets by the browser:

root.component.ts

                    .then(() => {
                        window.location.reload(true);
                    });

UPDATE 2:

According to the documentation of Angular, the Service Worker that is installed already on browsers of clients is cached. If you update the Service Worker code (like you did recently), then the new Service Worker code must be updated in all the browsers of the clients that have it already installed. Angular documentation states that there is a periodic check, but it doesn't specify it, that will check if the Service Worker itself needs an update and will do it. https://angular.io/guide/service-worker-devops#service-worker-updates

Perhaps, the manefistation of the issue only in the production and not in development or with a Private Browser mode (clean browser slate with no SW installed), seems that this is the case.

To verify this, you should compare the existing browser's SW that is running on the page using the developer tools of Chrome, with the updated SW code that should be installed. If they are different then it means that it hasn't been updated yet and that's what's causing the issue.

You can do this using a chrome browser that exhibits this issue and by going to Chrome Dev tools -> Application Tab -> Service Workers (in the top of left panel) and there will be the SW that is registered in the browser. Click the Source link and it will open up the actual Code of the SW. Compare the code line by line (or use a diff utility) with the updated SW code that should be running.

like image 88
J. Koutsoumpas Avatar answered Sep 29 '22 21:09

J. Koutsoumpas


Double check your cache headers. Chrome doesn't always listen to no-cache. If you've visited the site recently chrome will load index.html from cache. Causing the error.

We've changed our index.html to no-store and that seemed to solve the issue. https://gertjans.home.xs4all.nl/javascript/cache-control.html

like image 29
Raisins Avatar answered Sep 29 '22 22:09

Raisins