14

While there are similar posts, I can't find clear answer if index.html should be cached using Cache-Control header.

Correct me if I am wrong, but right now I am returning Cache-Control: no-store for index.html to avoid hash mismatch errors which forces service worker to go into degraded mode.

I think that if index.html which has Cache-Control: max-age=3600 is cached on CDN server and the app will be updated before the cache expires, ngsw.json will return different file hashes comparing to script files, included in index.html and bad things will happen. Right?

Also, just to make it clear, I have noticed some people add index.html to ngsw-config.json and that also does not make sense because index.html is loaded before the service worker.

Zygimantas
  • 8,547
  • 7
  • 42
  • 54
  • When you say `service worker to go into degraded mode`, do you mean that your app goes into one of the degraded states mentioned: https://angular.io/guide/service-worker-devops#driver-state? If so, then which one? – Krishnan Oct 20 '19 at 17:30
  • As for your other question, as per the documentation, adding `index.html` to `ngsw-config.json` is necessary for when the service worker needs to check for newer updated versions _once after the app has been loaded in browser_. If you do not add `index.html`, then the "build hash" will not be updated if there are changes in `index.html` in the future, which would put the service worker's cached resources in a bad state. – Krishnan Oct 20 '19 at 17:39
  • Yes, to EXISTING_CLIENTS_ONLY state – Zygimantas Oct 20 '19 at 17:50
  • 2
    Can you try adding `Cache-Control: no-store` to `ngsw.json` as well? I think what might be happening is that your `ngsw.json` file is being served with the older version possibly? If this works, next try removing `Cache-Control: no-store` from `index.html`. I think this should work too because Angular's Service Worker uses the cache-busters to get the new resources. Let me know if this works, I'll post this as an answer. – Krishnan Oct 20 '19 at 18:05
  • So, to answer your original question, I suppose, yes, `index.html` _can be_ cached in CDN. I guess, your concern with that is, "if I update my App to a new build version, then an already cached App on client browser will not be updated correctly because index.html is already cached", right? ... Reading through the documentation, it seems like Service Worker installs a different mechanism to check for updates, using `ngsw.json` manifest file. So, you only need to ensure that the CDN always serves the updated/non-cached version of `ngsw.json` file. – Krishnan Oct 20 '19 at 18:19
  • Let me test that, but you are probably right, by pointing to a reason why `index.html` should be in `ngsw.json`. Without it, ngsw may not know the hash of index.html and go into degraded state. – Zygimantas Oct 20 '19 at 20:49
  • Cool. Curious to know if it worked for you. Also, I think you would need to `cache-control: nocache` your ngsw.json file... UNLESS Angular fetches it with a cache-buster too. – Krishnan Oct 22 '19 at 08:36
  • @Krishnan, how did you append a header to the ngsw.json file to say ```Cache-Control: no-store```? – Willie Mar 31 '22 at 14:31
  • 1
    It depends on your backend, which serves static file. There are too many oprions to describe. It may be a configuration of your webserver, as well as CDN. – Zygimantas Apr 01 '22 at 05:51
  • I concur ^^. The header has to be configured in your server. – Krishnan Apr 02 '22 at 05:51

3 Answers3

4

By default, index.html is included. If you don't include it in the manifest, then it's not going to be part of the files hashed and checked. If it's not in the manifest (and subsequently, ngsw.json), changes to index.html won't trigger an event in the service worker. Of course, when you next load/refresh the site, it'll pick up the new index.html.

If you're serving index.html out of a CDN, then presumably, it's part of the distribution you built on the last deployment. It should be correctly calculated. The area you highlighted above is important to understand if you have files that don't match their hash in ngsw.json. If, for some reason, you're modifying index.html without updating your whole distro, service worker will assume the file is corrupted. It'll try again; since the file doesn't match the hash in ngsw.json, SW will assume the second try was corrupted and shut down.

In my case, it was because the application contained tokens left in during build which were replaced in the release pipeline with Azure resource keys. When the app was built, the hashes were correct. In the release, after token replacement was run, my main*.js files were no longer consistent with their hash values in ngsw.json. The way I elected to fix it was to add a powershell step and recalculate the hashes. It's important to note that, while the actual filenames have unique hash? code embedded, you do not have to correct that for the service worker to work. The filename/hash key/value pair must point to a valid file, and the SHA1 hash of that file must match what is in ngsw.json. The script I wrote to do post-compile validation/correction of the hashes is below. If you have some process that updates index.html independently of the entire distro, use this script to update the ngsw.json and include it with your index.html push.

Notes:

  • script accepts 3 parameters. If they're not passed, it assumes:
    • the script is being run from the root of the angular project
    • the working directory is "./dist" (where the scripts to be checked are)
    • the input path is "<working_dir>/ngsw.json"
    • the output path is "<working_dir>/ngsw_out.json"
  • Make sure you specify the same input path and output path if you want to modify the file
  • if you put this in AzDO, you'll need to check the "use Powershell Core" checkbox.

Powershell script begins:

param([string]$working_path = "./dist"
  , [string]$input_file_path = "$working_path/ngsw.json"
  , [string]$output_file_path = "$working_path/ngsw_out.json")

"Checking for existence of hash script..."

$fileExists = Test-Path -Path $input_file_path

if ($fileExists) {
  "Service Worker present.  Beginning hash reconciliation."
  ""
  $files_to_calc = @()
  $ngsw_json = (Get-Content $input_file_path -Raw) | ConvertFrom-Json

  "-----------------------------------------"
  "Getting list of javascript files to check"
  "-----------------------------------------"
  $found_count = 0
  for ($idx = 0; $idx -lt $ngsw_json.hashtable.psobject.properties.name.count; $idx++) {
    $current_file = $ngsw_json.hashtable.psobject.properties.name[$idx]
    if ($current_file.Contains(".js")) {
      $files_to_calc += $current_file
      "   File {$idx} $($files_to_calc[-1]) found."
      $found_count++
    }
  }

  "---------------------------------------"
  "$($files_to_calc.count) files to check."
  "---------------------------------------"
  $replaced_count = 0
  $files_to_calc | ForEach-Object {
    $new_hash_value = (Get-FileHash -Algorithm SHA1 "$($working_path)$_").Hash.ToLower()
    $current_hash_value = $ngsw_json.hashTable.$_
    $current_index = [array]::IndexOf($ngsw_json.hashTable.psobject.properties.name, $_)
    $replaced = $false

    if ($ngsw_json.hashTable.$_ -ne $new_hash_value) {
      $ngsw_json.hashTable.$_ = "$new_hash_value"
      $replaced = $true
      $replaced_count++
    }

    "$($replaced ? '** ' : '   '){$current_index}:$_ --- Current Value: " +
    "$($current_hash_value.substring(0, 8))... New Value: " +
    "$($new_hash_value.substring(0, 8))..."

  }
  ""
  "--> Replaced $replaced_count hash values"

  $ngsw_json | ConvertTo-Json -depth 32 | set-content "$output_file_path"
}
else {
  "Service Worker missing.  Skipping."
}
Tom Marks
  • 66
  • 4
1

I am not an expert on this but I am pretty sure that following links will help you with your doubts.

https://angular.io/guide/service-worker-getting-started#whats-being-cached

What's being cached?

Notice that all of the files the browser needs to render this application are cached. The ngsw-config.json boilerplate configuration is set up to cache the specific resources used by the CLI:

  • index.html.

  • favicon.ico.

  • Build artifacts (JS and CSS bundles).

  • Anything under assets.

  • Images and fonts directly under the configured outputPath (by default ./dist//) or resourcesOutputPath. See ng build for more information about these options.

And the below link has info about Service worker and caching of app resources. from which I would like you to read about App versions, Update checks and Resource integrity.

https://angular.io/guide/service-worker-devops#service-worker-and-caching-of-app-resources

I am also pasting the content of these three section here just to avoid making this answer "a link only answer"

App versions

In the context of an Angular service worker, a "version" is a collection of resources that represent a specific build of the Angular app. Whenever a new build of the app is deployed, the service worker treats that build as a new version of the app. This is true even if only a single file is updated. At any given time, the service worker may have multiple versions of the app in its cache and it may be serving them simultaneously. For more information, see the App tabs section below.

To preserve app integrity, the Angular service worker groups all files into a version together. The files grouped into a version usually include HTML, JS, and CSS files. Grouping of these files is essential for integrity because HTML, JS, and CSS files frequently refer to each other and depend on specific content. For example, an index.html file might have a tag that references bundle.js and it might attempt to call a function startApp() from within that script. Any time this version of index.html is served, the corresponding bundle.js must be served with it. For example, assume that the startApp() function is renamed to runApp() in both files. In this scenario, it is not valid to serve the old index.html, which calls startApp(), along with the new bundle, which defines runApp().

This file integrity is especially important when lazy loading modules. A JS bundle may reference many lazy chunks, and the filenames of the lazy chunks are unique to the particular build of the app. If a running app at version X attempts to load a lazy chunk, but the server has updated to version X + 1 already, the lazy loading operation will fail.

The version identifier of the app is determined by the contents of all resources, and it changes if any of them change. In practice, the version is determined by the contents of the ngsw.json file, which includes hashes for all known content. If any of the cached files change, the file's hash will change in ngsw.json, causing the Angular service worker to treat the active set of files as a new version.

With the versioning behavior of the Angular service worker, an application server can ensure that the Angular app always has a consistent set of files.

Update checks

Every time the user opens or refreshes the application, the Angular service worker checks for updates to the app by looking for updates to the ngsw.json manifest. If an update is found, it is downloaded and cached automatically, and will be served the next time the application is loaded.

Resource integrity

One of the potential side effects of long caching is inadvertently caching an invalid resource. In a normal HTTP cache, a hard refresh or cache expiration limits the negative effects of caching an invalid file. A service worker ignores such constraints and effectively long caches the entire app. Consequently, it is essential that the service worker gets the correct content.

To ensure resource integrity, the Angular service worker validates the hashes of all resources for which it has a hash. Typically for an app created with the Angular CLI, this is everything in the dist directory covered by the user's src/ngsw-config.json configuration.

If a particular file fails validation, the Angular service worker attempts to re-fetch the content using a "cache-busting" URL parameter to eliminate the effects of browser or intermediate caching. If that content also fails validation, the service worker considers the entire version of the app to be invalid and it stops serving the app. If necessary, the service worker enters a safe mode where requests fall back on the network, opting not to use its cache if the risk of serving invalid, broken, or outdated content is high.

Hash mismatches can occur for a variety of reasons:

  • Caching layers in between the origin server and the end user could serve stale content.
  • A non-atomic deployment could result in the Angular service worker having visibility of partially updated content.
  • Errors during the build process could result in updated resources without ngsw.json being updated. The reverse could also happen resulting in an updated ngsw.json without updated resources.
HirenParekh
  • 3,655
  • 3
  • 24
  • 36
  • 2
    Thank you for your answer. The last part, "Resource integrity" is the closest to my question, but still leaves it unanswered: `"If a particular file fails validation, the Angular service worker attempts to re-fetch the content using a "cache-busting" URL parameter to eliminate the effects of browser or intermediate caching. If that content also fails validation, the service worker considers the entire version of the app to be invalid and it stops serving the app."`. So, if CDN server returns outdated index.html and ngsw.json?cache-busted is up to date, the service worker is in trouble. – Zygimantas Oct 17 '19 at 08:42
0

I think it is necessary for you to understand about Angular app work flow and Angular Service Worker runtime caching mechanism. So I'm going to write down about them here. It will be help for your understanding.

Angulars start working with following steps.

  • Angular starts with main.ts.
  • Angular app is bootstrapped and app.module.ts is passed as an argument.
  • And Angular analyze app component, reading the set up passed there and there is SELECTOR app-root.
  • Now, Angular is enable to handle app-root in the index.html and knows rules for the SELECTOR.
  • SELECTOR should insert the app components and have some HTML code - a template attached to him - html component.

Angular ServiceWorker

Angular CLI has also included in Angular application root module the Service Worker module. The CLI has also added a new configuration file called ngsw-config.json, which configures the Angular Service Worker runtime behavior, and the generated file comes with some intelligent defaults. There is a lot going on here, so let's break it down step-by-step. This file contains the default caching behavior or the Angular Service Worker, which targets the application static asset files: the index.html, the CSS and Javascript bundles.

The Angular Service Worker can cache all sorts of content in the browser Cache Storage. This is a Javascript-based key/value caching mechanism that is not related to the standard browser Cache-Control mechanism, and the two mechanisms can be used separately.

The files under the app section are the application: a single page is made of the combination of its index.html plus its CSS and Js bundles. These files are needed for every single page of the application and cannot be lazy loaded.

In the case of these files, we want to cache them as early and permanently as possible, and this is what the app caching configuration does. The app files are going to be proactively downloaded and installed in the background by the Service Worker, and that is what the install mode prefetch means. The Service worker will not wait for these files to be requested by the application, instead, it will download them ahead of time and cache them so that it can serve them the next time that they are requested. This is a good strategy to adopt for the files that together make the application itself (the index.html, CSS and Javascript bundles) because we already know that we will need them all the time.


index.html depends on index.js which depends on chunk.js which depends on jquery.js. chunk is loaded from the browser cache.

Amir Christian
  • 597
  • 6
  • 20
  • Thank you, I am aware of basics on how Angular works and what is the purpose of service worker. The question is: how `ngsw` behaves once it gets a reference of `v2 of index.html`, but CDN still serves `v1 of index.html` and cache busting does not help? I've seen that it goes into degraded state and never comes back without clearing the browser cache. – Zygimantas Oct 20 '19 at 20:46
  • @zygimantas, ngsw and CDN work separately. CDN resources from index.html is loaded from browser cache but you set Cache-Control: no-store so it wasn't updated I think. – Amir Christian Oct 20 '19 at 23:13