Skip to content

Debug ID changes the bytes of polyfills and main but not their file names, so cached files fail the SRI check #34674

Open

Description

@jsaguet

Command

build

Is this a regression?

Yes. It started with the debug IDs in 22.1.0.

Description

Follow-up to #34574.

The builder names polyfills-<hash>.js and main-<hash>.js first. Then it adds a //# debugId=<id> line to the file. It computes <id> from the source map after it rewrites the map to add x_google_ignoreList. The file name does not cover that line.

So when a builder release writes the source map differently, the JS bytes change and the file name stays the same. 22.1.8 did that. Commit 5a0d226 moved x_google_ignoreList to the start of the map.

With subresourceIntegrity, the new index.html pins the new bytes. A CDN or browser cache that kept the old file under the same URL serves the old bytes, and the browser blocks the script. This happened to us in production with polyfills-<hash>.js.

#34583 fixed this for the chunks of the chunk optimizer. It does not cover files that esbuild names, such as main and polyfills. 22.2.6 still computes their debug ID from the rewritten map.

Minimal Reproduction

npx @angular/cli@22.1.8 new repro --defaults --ssr=false
cd repro
npm install zone.js
npm install -D @angular/build@22.1.6
npx ng config projects.repro.architect.build.options.polyfills '["zone.js"]'
npx ng config projects.repro.architect.build.configurations.production.sourceMap true
npx ng config projects.repro.architect.build.configurations.production.subresourceIntegrity true
npx ng build && mv dist/repro/browser ../build-22.1.6
npm install -D @angular/build@22.1.8
npx ng build && mv dist/repro/browser ../build-22.1.8

Both builds write polyfills-QWJLBG44.js and main-HHND32NM.js. In each file, only the //# debugId= line differs:

polyfills-QWJLBG44.js
  build-22.1.6: //# debugId=d932f97d-b667-5462-b3b7-b328a1ce7f5a
  build-22.1.8: //# debugId=81c8766a-7d41-50fd-ac6d-16c3c4ed6358
main-HHND32NM.js
  build-22.1.6: //# debugId=e3a30145-1442-57c2-96a7-a88f9bf43b0a
  build-22.1.8: //# debugId=50dd2d3c-f1e3-5224-9587-43f24f562421

The two index.html files pin different hashes for the same URLs. For both files, the two source maps have the same content in a different key order.

Exception or Error

Failed to find a valid digest in the 'integrity' attribute for resource 'https://example.com/polyfills-JTOAPWZJ.js' with computed SHA-384 integrity '...'. The resource has been blocked.

Your Environment

Angular CLI: 22.1.8
@angular/build: 22.1.6, then 22.1.8
@angular/core: 22.2.5
zone.js: 0.16.3
Node: 24.17.0
OS: macOS

Anything else relevant?

Expected: when the bytes of a hashed file change, its name changes too.

A possible fix is to compute the debug ID from the JS before the builder adds the debug ID line, not from the rewritten map. The file name already covers the JS.

Activity

  1. added this to the needsTriage milestone on Nov 8, 2026
  2. changed the title [-]Debug ID changes the bytes of main and polyfills but not their file names, so cached files fail the SRI check[/-] [+]Debug ID changes the bytes of polyfills and main but not their file names, so cached files fail the SRI check[/+] on Nov 8, 2026
  3. jsaguet commented on Nov 8, 2026

    @jsaguet
    ContributorAuthor

    The same symptom has a second cause: the entry file of the chunk optimizer. The debug ID fix above does not fix it.

    The chunk optimizer runs when an app has 3 or more lazy chunks. It renames the lazy chunks, and main imports them by name. But the optimizer passes chunkFileNames and no entryFileNames, so main keeps the name that esbuild gave it.

    So when a builder release changes how the optimizer names chunks, the bytes of main change and its name stays the same. 22.2.6 did that with #34583.

    npx @angular/cli@22.2.5 new repro --defaults --ssr=false
    cd repro
    npm install -D @angular/build@22.2.5
    npx ng generate component a
    npx ng generate component b
    npx ng generate component c
    cat > src/app/app.routes.ts <<'EOF'
    import { Routes } from '@angular/router';
    
    export const routes: Routes = [
      { path: 'a', loadComponent: () => import('./a/a').then((m) => m.A) },
      { path: 'b', loadComponent: () => import('./b/b').then((m) => m.B) },
      { path: 'c', loadComponent: () => import('./c/c').then((m) => m.C) },
    ];
    EOF
    npx ng config projects.repro.architect.build.configurations.production.sourceMap true
    npx ng config projects.repro.architect.build.configurations.production.subresourceIntegrity true
    npx ng build && mv dist/repro/browser ../build-22.2.5
    npm install -D @angular/build@22.2.6
    npx ng build && mv dist/repro/browser ../build-22.2.6

    Both builds write main-5H54S2WB.js, and the two index.html files pin different hashes for it. The only difference in the file is the names of the chunks it imports:

    build-22.2.5: chunk-jbVvGIzd.js chunk-BldQ8CJu.js chunk-BsxYmvae.js
    build-22.2.6: chunk-Dn-U95iY.js chunk-Da40IlVd.js chunk-DZObh1Dd.js
    

    The debug ID and the source map of main are the same in both builds.

    A possible fix is to give the entry a content hash with entryFileNames, the same way as chunkFileNames, and to update index.html with the new name. A fix that computes every file name from its final bytes would cover both causes.

  4. jsaguet commented on Nov 8, 2026

    @jsaguet
    ContributorAuthor

    I looked at how to fix both causes. The fix for the second cause is small. The fix for the first cause has a trade-off that a maintainer should decide.

    The rule I want to keep is that when the bytes of a hashed file change, its name changes too. That rule must also hold on the upgrade to the release that contains the fix.

    Second cause: the entry file of the chunk optimizer

    The fix passes entryFileNames to the three generate() calls in chunk-optimizer.ts, so main gets a content hash when bundles are hashed. initialFiles then uses the new name. With outputHashing: none, the file stays main.js.

    When the optimizer runs, the name of main can now contain lowercase letters, _, and -, as the names of the lazy chunks already do.

    First cause: the debug ID

    The builder adds the debug ID line after esbuild picks the file name. If the builder computes the ID in a new way, the bytes of each file change and its name stays the same. The release that fixes this bug would therefore cause the same SRI failure one more time. It would affect apps that use script source maps and SRI and still have the old files in a cache.

    I see these options:

    1. Compute the debug ID from the file name and the JS. A later rewrite of the source map can no longer change the JS. The upgrade still breaks the rule once, so the release would need a note that tells users to purge cached JS.
    2. Do option 1 and force a one-time rename. This needs a version comment in the esbuild bundles and a constant in augmentChunkHash. The upgrade is safe, but the bundles carry a comment that exists only to change a hash. I don't like this option.
    3. Keep the current debug ID and name the files from their final bytes. This is the option I prefer.

    Option 3 adds a step after injectDebugIds, only when bundles are hashed and script source maps are on. The step hashes each JS file with the names of the other output files replaced by a placeholder. It includes the hashes of the files that the JS file can reach through imports, as Rollup does to handle import cycles. It then renames the files and updates those names in the JS and the source maps. initialFiles and the metafile get the new names, so the index HTML, SRI, the SSR manifest, and the service worker use them.

    With option 3, every file with a debug ID gets a new name once, on the upgrade. After that, any change to the bytes of those files changes the name, whatever the cause. The cost is more code, because the builder would rewrite file names inside generated code. Each locale also needs its own copy of initialFiles and the metafile, and the build stats table must print the final names.

    Keeping the current debug ID without option 3 avoids the upgrade problem, but the bug comes back the next time the builder writes the source map differently.

    Would you accept a rename step like option 3 in the application builder? I have not built it yet. If you would not, which other option do you prefer? I can open a PR for the second cause now and a PR for the first cause once you pick a direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions