Skip to content

Debug ID changes the bytes of an optimized chunk but not its file name, so cached files fail the SRI check #34574

Open

Description

@virgiliojr

Command

build

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

22.0.x

Description

Since 22.1.0 (#33110), the application builder adds a //# debugId=<uuid> line to each browser JS file when script source maps are enabled. The ID comes from the bytes of the source map.

When the chunk optimizer runs (by default when the build has 3 or more lazy chunks), Rolldown gives the chunks their final file names. The [hash] in these names covers the code of the chunk only. It does not cover the source map. The debugId line is added after that.

A change that keeps the JS code of a chunk the same but changes its source map (for example, a change to a TypeScript type or to a comment) gives this result for a chunk that does not import other chunks:

  1. The file name stays the same.
  2. The debugId changes, because the source map changed.
  3. The file bytes change, because the debugId line changed.
  4. The SRI hash in index.html changes to match the new bytes.

In one build, the HTML and the files match. Across two builds, the rule "same hashed file name means same content" is no longer true.

Hosting setups depend on this rule: they cache hashed files as immutable. Any cache can keep the file from the previous build: a CDN such as Cloudflare, the browser cache, or a proxy. That cache then serves the old bytes together with the new index.html. The integrity check fails, the browser blocks the file, and the modules in it do not load.

Before 22.1.0 there was no debugId line, so the bytes of a hashed file changed only when its name changed.

Expected behavior: if the bytes of a hashed output file change, its file name changes too.

Minimal Reproduction

Repository: https://github.com/virgiliojr/angular-debugid-sri-repro

The app is a plain ng new app (Angular CLI 22.2.2) with 3 lazy routes, so the chunk optimizer runs. lazy-a and lazy-b use src/app/shared/label.ts. This module has no imports, so it becomes its own chunk.

  1. Run npm ci.
  2. Run npx ng build --source-map --subresource-integrity.
  3. In dist/repro/browser/, find the chunk that contains label:. Write down its file name and its SHA-384.
  4. In src/app/shared/label.ts, add extra?: number; to the LabelInput interface. This is a type-only change: the JS output of label.ts stays the same.
  5. Run npx ng build --source-map --subresource-integrity again.
  6. Find the chunk that contains label: again.

bash compare-builds.sh in the repository does steps 2 to 6 and prints the result below.

Actual result:

Build 1
  file name:          chunk-z-ytF2G0.js
  sha384 of bytes:    sha384-Y7f5K4V0FTCrXc6JQ2tGE+VllRWg501EixeFEPMjNMboDgwfeRc9E6l8oHlqTbX5
  debugId line:       //# debugId=105cedd6-2603-584f-8e8d-7801d6020b96
  sha in index.html:  yes
Build 2
  file name:          chunk-z-ytF2G0.js
  sha384 of bytes:    sha384-bgCbCJmJexbhKCHeS4kpofe2lKBPWJ1f3JpK9668+/jD2mfiEYADUy5xD1Usr1dZ
  debugId line:       //# debugId=9c547775-c029-5af8-ba7e-25c70949f1b6
  sha in index.html:  yes

Diff of the chunk between the two builds:
2c2
< //# debugId=105cedd6-2603-584f-8e8d-7801d6020b96
---
> //# debugId=9c547775-c029-5af8-ba7e-25c70949f1b6

The file name is the same, but the bytes and the SRI hash in index.html are different.

Expected result: if the bytes are different, the file name is different too.

To see the failure in a browser, serve the output of build 2, but serve chunk-z-ytF2G0.js from build 1. This is what a cache does after a deployment.

Exception or Error

Failed to find a valid digest in the 'integrity' attribute for resource 'https://<host>/chunk-z-ytF2G0.js' with computed SHA-384 integrity '<hash>'. The resource has been blocked.

Your Environment

Angular CLI       : 22.2.5
Angular           : 22.2.3
Node.js           : 24.18.0
Package Manager   : npm 11.16.0
Operating System  : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build            │ 22.2.5            │ ^22.2.2           │
│ @angular/cli              │ 22.2.5            │ ^22.2.2           │
│ @angular/common           │ 22.2.3            │ ^22.2.0           │
│ @angular/compiler         │ 22.2.3            │ ^22.2.0           │
│ @angular/compiler-cli     │ 22.2.3            │ ^22.2.0           │
│ @angular/core             │ 22.2.3            │ ^22.2.0           │
│ @angular/forms            │ 22.2.3            │ ^22.2.0           │
│ @angular/platform-browser │ 22.2.3            │ ^22.2.0           │
│ @angular/router           │ 22.2.3            │ ^22.2.0           │
│ rxjs                      │ 7.8.2             │ ~7.8.0            │
│ typescript                │ 6.0.3             │ ~6.0.2            │
└───────────────────────────┴───────────────────┴───────────────────┘

Anything else relevant?

This does not depend on the browser. Any cache in front of the hashed files can cause it.

The defect needs the chunk optimizer. With NG_BUILD_OPTIMIZE_CHUNKS=false, the same type change gives a new esbuild file name for the chunk (for example chunk-P54LQIKA.js to chunk-5HS55H7R.js), so the name and the bytes change together.

A chunk that imports other chunks usually gets a new name anyway, because the names of the chunks it imports change. A chunk without such imports keeps its name.

Related: #33108, #33110, #33572. The current main still has this behavior: injectDebugIds (called from execute-post-bundle.ts when sourcemapOptions.scripts is true) changes the file contents but does not change the file name. The Rolldown pass in chunk-optimizer.ts uses chunkFileNames: '<name>-[hash].js'.

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