Debugging SRI Validation Failures
The browser console message “Failed to find a valid digest in the integrity attribute” — or its network-panel equivalent, a blocked resource with a net::ERR_SRI_MISMATCH error — means the bytes received from the network do not hash to the value recorded in the integrity attribute. Pinpointing which side is wrong requires isolating whether the problem is in the served bytes, the attribute value, or the fetch configuration.
Symptom and Decision Framing
SRI failures surface in two distinct places:
- Console error:
Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/assets/main.a1b2c3d4.js' with computed SHA-384 integrity 'sha384-XXXX...'. The resource has been blocked. - Network panel: The request shows status
(blocked:sri)in Chrome DevTools or(SRI)in Firefox.
A blocked resource means one of four root causes: the bytes were altered after the hash was computed, the hash was computed with the wrong algorithm or wrong input, the crossorigin attribute is absent on a cross-origin load, or the integrity attribute value is stale from a previous build. Each cause has a different fix. Work through the diagnosis table before editing any configuration.
Diagnosis Decision Table
| Symptom | Likely cause | First check |
|---|---|---|
| Failure only on CDN-served assets, not on local dev | CDN content transform (minification, Brotli re-encoding) | Compare curl --compressed bytes locally vs from CDN |
| Failure immediately after a rebuild with no CDN change | Stale integrity attribute in HTML — old hash, new file | Re-run build and SRI generation together; check HTML was updated |
| Failure on cross-origin assets only | Missing crossorigin attribute |
Inspect the blocked <script> or <link> tag in DevTools Elements panel |
| Failure on all assets including same-origin | Wrong algorithm in integrity prefix |
Check sha256- vs sha384- vs sha512- prefix in the attribute |
| Failure only on dynamically imported chunks | Build plugin did not patch runtime | Verify crossOriginLoading: 'anonymous' in Webpack output config |
| Failure after CDN migration (e.g., moved from Nginx to Cloudflare) | New CDN strips/adds headers, alters body | Re-hash the deployed asset with curl and compare |
| Failure on CSS but not JS | CSS processing pipeline produces non-deterministic output | Check PostCSS autoprefixer, CSS Modules scoping, or source-map injection |
Cause 1: CDN Content Transforms
This is the most common production SRI failure and the hardest to catch in development.
Cloudflare’s “Auto Minify” feature, Amazon CloudFront’s “Compress Objects Automatically” setting, and Nginx gzip_vary with certain proxy configurations can all alter the decompressed response body. The browser’s SRI check operates on the decompressed bytes — transport-layer compression (gzip/Brotli via Content-Encoding) is transparent. But if the CDN strips comments, rewrites whitespace, or changes the character encoding of a CSS file, the decompressed payload differs from the file you hashed at build time.
Verify with curl:
# Download the CDN-served asset (decompressed by --compressed)
curl -s --compressed "https://cdn.example.com/assets/main.a1b2c3d4.js" \
-o /tmp/cdn-main.js
# Hash what you built locally
LOCAL_HASH=$(openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A)
# Hash what the CDN served
CDN_HASH=$(openssl dgst -sha384 -binary /tmp/cdn-main.js | openssl base64 -A)
echo "Local: sha384-$LOCAL_HASH"
echo "CDN: sha384-$CDN_HASH"
[ "$LOCAL_HASH" = "$CDN_HASH" ] && echo "MATCH — CDN is not transforming" \
|| echo "MISMATCH — CDN is modifying bytes"
If they differ, find the transform:
# Diff to see what changed
diff <(xxd dist/assets/main.a1b2c3d4.js) <(xxd /tmp/cdn-main.js) | head -40
The diff is usually anticlimactic and always decisive: a licence banner removed, a trailing newline dropped, a sourceMappingURL comment rewritten to an absolute path. Any single character is enough — a digest has no notion of a small change.
Resolution by CDN:
# Cloudflare: disable Auto Minify via API for a specific zone
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/minify" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": {"css": "off", "html": "off", "js": "off"}}'
For CloudFront, set the cache behavior’s “Compress Objects Automatically” to No for the /assets/* path pattern in your distribution configuration.
Cause 2: Missing crossorigin Attribute
Without crossorigin, the browser sends an opaque request for a cross-origin resource. An opaque response has no readable body — the browser cannot pass the bytes through the hash function. The integrity check therefore fails even when the bytes are identical to what was hashed.
Check the HTML tag in DevTools:
- Open DevTools → Elements panel
- Find the
<script>or<link>tag that is blocked - Confirm whether
crossoriginis present
Correct form for a CDN-hosted asset:
<!-- WRONG: missing crossorigin on cross-origin load -->
<script src="https://cdn.example.com/assets/main.a1b2c3d4.js"
integrity="sha384-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU96xtjz3FAPZ3KZHA12iK">
</script>
<!-- CORRECT -->
<script src="https://cdn.example.com/assets/main.a1b2c3d4.js"
integrity="sha384-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU96xtjz3FAPZ3KZHA12iK"
crossorigin="anonymous">
</script>
For same-origin assets (document and asset share the same scheme+host+port), crossorigin is not required for the SRI check to work. But adding crossorigin="anonymous" to same-origin assets is harmless and avoids confusion if the asset is later moved to a CDN.
Also verify the CDN responds with Access-Control-Allow-Origin:
curl -I -H "Origin: https://www.example.com" \
"https://cdn.example.com/assets/main.a1b2c3d4.js" \
| grep -i "access-control"
# Expected: Access-Control-Allow-Origin: * (or https://www.example.com)
If the CDN does not return that header for this origin, the response never becomes readable and SRI cannot check it. The header requirements per provider, and the way Vary: Origin interacts with edge caching once you stop using a wildcard, are covered in SRI with crossorigin and CDN CORS headers.
Cause 3: Stale Integrity Value After Rebuild
If your build pipeline computes the SRI hash in one step and updates the HTML template in a separate step — and those steps can be run independently — you can end up with a new asset file but an old integrity value in the HTML.
Check by comparing the HTML’s integrity attribute against the current file:
# Extract the integrity value from the deployed HTML
DEPLOYED_INTEGRITY=$(curl -s "https://www.example.com/" \
| grep -o 'integrity="[^"]*"' | head -1 | sed 's/integrity="//;s/"//')
# Compute the expected integrity from the local build
EXPECTED_INTEGRITY="sha384-$(openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js \
| openssl base64 -A)"
echo "Deployed: $DEPLOYED_INTEGRITY"
echo "Expected: $EXPECTED_INTEGRITY"
[ "$DEPLOYED_INTEGRITY" = "$EXPECTED_INTEGRITY" ] \
&& echo "MATCH — HTML and asset are in sync" \
|| echo "MISMATCH — HTML has stale integrity value"
The fix is to couple SRI generation and HTML update into a single atomic step. In your package.json:
{
"scripts": {
"build": "vite build && node scripts/sri-manifest.mjs && node scripts/inject-sri.mjs"
}
}
Never deploy HTML and assets as separate steps if the HTML contains integrity attributes — both must update together. See CI/CD asset pipeline integration for atomic deploy patterns.
Cause 4: Wrong Hash Algorithm
The integrity attribute prefix must exactly match the algorithm used to compute the hash. If your build pipeline computed sha256 but the attribute prefix says sha384, the browser computes SHA-384 over the bytes, finds no matching value in the attribute, and blocks the resource.
Verify the algorithm:
# What algorithm is in the HTML?
curl -s "https://www.example.com/" | grep -o 'integrity="[^"]*"'
# Example output: integrity="sha256-abc123..."
# Compute with the same algorithm and compare
openssl dgst -sha256 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A
For the MD5 vs SHA-256 for assets context: MD5 is not a valid SRI algorithm. Browsers will reject integrity="md5-..." as an unrecognized algorithm and block the resource. Only sha256, sha384, and sha512 are accepted.
crossorigin presence, then algorithm prefix, then whether the integrity value is stale from a prior build.DevTools Verification Workflow
Chrome DevTools provides the most diagnostic detail for SRI failures:
- Network panel → find the blocked request. Status shows
(blocked:sri). Click the request row. - Headers tab → Response Headers. Confirm
Access-Control-Allow-Originis present. If it is missing,crossoriginis the problem regardless of whether the hash is correct. - Preview tab. If the response body is visible here, the fetch succeeded but the integrity check failed — meaning the bytes differ from the hash. If the preview shows an error, the CORS check failed before SRI could run.
- Console. The full message includes the computed hash:
...with computed SHA-384 integrity 'sha384-XXXXXX'. Copy this value and compare it against theintegrityattribute in the Elements panel.
In Firefox, the Network panel marks blocked requests with [SRI] in the Transferred column. The Console error includes the same computed-hash detail as Chrome.
# Replicate the browser's exact hash computation (cross-platform)
# Downloads with Accept-Encoding to trigger CDN compression response
# then decompresses (--compressed) before hashing — same path as the browser
curl -s \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Origin: https://www.example.com" \
--compressed \
"https://cdn.example.com/assets/main.a1b2c3d4.js" \
| openssl dgst -sha384 -binary | openssl base64 -A
Compare the output against the integrity attribute in the HTML. If they match, the problem is crossorigin or CORS headers. If they differ, the CDN is modifying bytes.
Cause 5: Dynamic Import Chunks Without Runtime Patching
Applications using import() for code splitting generate chunks at build time, but those chunks are loaded at runtime by bundler-generated boilerplate. Without explicit runtime patching, the dynamically constructed <script> tags that load these chunks do not carry integrity attributes.
For Webpack, the fix is crossOriginLoading: 'anonymous' in output config combined with the webpack-subresource-integrity plugin, which patches __webpack_require__.l. Without both, the plugin can set integrity on entry chunks in HTML but cannot reach the runtime loader.
Verify which chunks are covered:
# Check the built HTML for integrity attributes
grep -c 'integrity=' dist/index.html
# Should be >= the number of entry scripts + stylesheets
# Check if the Webpack runtime includes integrity data
grep -c '__webpack_require__.*integrity' dist/assets/runtime.*.js
# Should be > 0 if webpack-subresource-integrity patched the runtime
Narrowing the Blast Radius
When the four named causes all check out and the resource is still blocked, stop guessing and bisect the delivery path. Every hop between the bundler and the JavaScript engine is a place where a byte can change, and each one can be hashed independently. Compute the digest at the earliest hop you control, then at each subsequent hop, and stop at the first place where the value moves — that hop owns the bug.
The last two hops explain most “works for everyone except one customer” reports. TLS-inspecting middleboxes and enterprise antivirus proxies routinely re-serialise JavaScript, and some browser extensions inject into script bodies before execution. Neither is fixable from your side, which is why an error-event beacon that records the user agent and the failing URL is worth more than another round of build changes — it tells you immediately whether a failure is global or confined to one network.
Frequently Asked Questions
Why does the console show a computed hash that matches nothing I generated?
The value in the message is the digest of the bytes the browser actually received, so it is the single most useful number in the whole failure. Hash the deployed file with curl --compressed piped to openssl; if your command reproduces the browser’s value, the served bytes are the problem. If it does not, something between the CDN and that specific client altered the response.
Can a failure be caused by the HTML rather than the asset?
Yes, and it is common after a partial deploy. The asset can be entirely correct while the HTML carries an integrity value from an earlier build — the fingerprinted URL and the digest come from the same source bytes, so a mismatch between them means one of the two artifacts is stale. Comparing the deployed HTML’s attribute against a freshly computed digest, as in the Cause 3 snippet, settles it in one command.
Does a blocked resource retry?
No. The browser discards the response and treats the element as failed; there is no automatic re-fetch and no fallback to a non-integrity load. Anything depending on that script simply never runs, which is why an SRI failure on an entry bundle presents as a blank page rather than a degraded one.
Is there a way to test integrity failures deliberately?
Change one character of the base64 digest in a local copy of the HTML and load it against your real CDN. You get the genuine console message, the genuine network-panel state, and a safe way to confirm your beacon and alerting actually fire — much better than discovering during an incident that nothing was listening.
When to Reconsider
Stop using SRI in these situations:
- CDN edge rendering or ESI — if the CDN assembles the HTML from fragments, the
integrityattribute in the final HTML may have been written by a different process than the one that computed the hash. - A/B test asset variants — if your feature flag system serves different asset bytes to different users from the same URL, a single integrity value cannot be correct for all variants. Use different URLs per variant instead.
- Assets served over HTTP (non-TLS) — browsers still check SRI over HTTP, but the channel is not secure so SRI provides limited protection. TLS is a prerequisite for SRI to be meaningful from a security standpoint.
- Frequently updated third-party assets — if you are embedding a third-party script whose hash changes on every vendor release, you will need to update your
integrityattribute on every vendor update. Evaluate whether the maintenance cost is worth the security benefit. For cache key architecture reasons, content-addressed first-party assets do not have this problem.
Related
- Subresource Integrity validation — the full SRI specification, browser verification flow, and attribute reference
- Generating SRI hashes in your build pipeline — automate hash generation with Webpack, Vite, and Node scripts
- MD5 vs SHA-256 for assets — algorithm selection and why MD5 is rejected by SRI
- Cache key architecture — how fingerprinted URLs and SRI work together for immutable caching