Search code examples
angularcachingservice-workercache-controlangular-service-worker

Angular service worker and index.html caching


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.


Solution

  • 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."
    }