SRI Fallback with Multiple CDN Sources
Permalink to "SRI Fallback with Multiple CDN Sources"Part of Graceful Fallback Strategies, this page shows how to fail over to a second CDN origin — each with its own integrity attribute — when the primary serves a blocked, tampered, or unreachable resource.
Quick Reference
Permalink to "Quick Reference"| Concern | Rule |
|---|---|
| Failure signal | onerror on the primary <script> fires on SRI block or network error |
| Fallback trigger | Inject a new <script> pointing at a second origin |
Per-source integrity |
Required on every source (primary, secondary, local) |
Per-source crossorigin |
crossorigin="anonymous" required on every cross-origin source |
| Hash reuse across origins | Same token works only if both origins serve byte-identical files |
| Secondary check | Verify the expected global (window.<lib>) after load |
| Last resort | Local self-hosted copy with its own token |
A script tag has one src, so the browser cannot try a second origin on its own — you drive the failover in an onerror handler.
What multi-source fallback protects against
Permalink to "What multi-source fallback protects against"A single CDN reference is a single point of failure. If that origin is unreachable, rate-limited, or — worse — serving bytes that no longer match your integrity token because of tampering or an edge transform, the browser blocks the script and any code depending on it throws. A multi-source fallback keeps the integrity guarantee intact while adding availability: when the primary is blocked, an onerror handler loads the identical library from an independent origin that carries its own hash. Because SRI is keyed to file content rather than to a URL, a byte-identical mirror validates against the same token, so you never trade security for uptime. The browser-side mechanics of how and when onerror fires are covered in Handling SRI Failures with onerror Handlers; this page focuses on wiring a second origin behind the first.
Canonical Implementation
Permalink to "Canonical Implementation"Load the primary from CDN A with its own token. Its onerror injects a byte-identical copy from CDN B, again with its own integrity and crossorigin. Both origins serve the same file, so the same token validates either way.
<script
src="https://cdn-a.example.com/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
onerror="loadFallback()"></script>
<script>
function loadFallback() {
var s = document.createElement('script');
s.src = 'https://cdn-b.example.com/lib.min.js';
// Same content on both origins → same token validates from either.
s.integrity = 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
s.crossOrigin = 'anonymous';
s.onerror = function () {
console.error('Both CDN sources failed for lib.min.js');
};
document.head.appendChild(s);
}
</script>
The crossOrigin property must be set before the element is appended, otherwise the browser initiates the fetch without CORS and the fallback is blocked exactly like an opaque response.
Variant Examples
Permalink to "Variant Examples"Presence check fallback (window.<lib>)
Permalink to "Presence check fallback (window.<lib>)" onerror catches a blocked or failed load, but not the case where a script loaded yet did not define the global you expected (a truncated or wrong build). Add a presence check that runs after the primary:
<script
src="https://cdn-a.example.com/jquery.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
<script>
// If the primary did not define window.jQuery, load the fallback origin.
if (!window.jQuery) {
var s = document.createElement('script');
s.src = 'https://cdn-b.example.com/jquery.min.js';
s.integrity = 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
}
</script>
Local self-hosted last resort
Permalink to "Local self-hosted last resort"When both public origins may be blocked (corporate proxies, ad blockers, regional restrictions), chain a same-origin local copy as the final tier. A same-origin file does not require crossorigin, but still carries its own token:
<script>
function loadLocal() {
var s = document.createElement('script');
s.src = '/vendor/lib.min.js'; // same-origin, self-hosted
s.integrity = 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
// No crossorigin needed for same-origin; the browser can read the body.
document.head.appendChild(s);
}
</script>
Distinct tokens per origin
Permalink to "Distinct tokens per origin"If your mirrors are not guaranteed byte-identical (different compression re-encoding at rest, differing build), give each origin its own token and select the token with the URL:
const SOURCES = [
{ src: 'https://cdn-a.example.com/lib.min.js', integrity: 'sha384-AAA…' },
{ src: 'https://cdn-b.example.com/lib.min.js', integrity: 'sha384-BBB…' },
];
function loadFrom(i) {
if (i >= SOURCES.length) return console.error('All sources exhausted');
const s = document.createElement('script');
s.src = SOURCES[i].src;
s.integrity = SOURCES[i].integrity;
s.crossOrigin = 'anonymous';
s.onerror = () => loadFrom(i + 1);
document.head.appendChild(s);
}
loadFrom(0);
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Omitting
crossorigin="anonymous"on the fallback silently defeats it. This is the single most common bug in multi-source fallback: the primary is tagged correctly, but the injected fallback tag setsintegritywithoutcrossorigin. The browser then makes a no-CORS request, receives an opaque response it cannot read, and blocks the fallback exactly as it blocked the primary — so your “resilience” layer never runs. Every source, including dynamically injected ones, needscrossoriginset before insertion. -
A shared token only works if the bytes are truly identical. Two CDNs mirroring the “same” library can differ by a trailing newline, a different minifier, or version drift. If they are not byte-identical, a single token validates on one origin and blocks the other. Verify with
curl … | openssl dgstagainst both, or give each origin its own token. -
onerrordoes not fire for a script that loads but is semantically wrong. A 200 response that defines nothing useful passes SRI (if the hash matches) yet leaves your app broken. Paironerrorwith awindow.<lib>presence check. -
Setting
integrity/crossOriginafterappendChildhas no effect. The fetch begins at insertion, so configure the element fully first. -
Do not fall back to an untrusted origin without a hash. Failing over to a public registry URL with no
integritytrades a availability problem for a supply-chain hole. Every tier must be pinned.
Verification Steps
Permalink to "Verification Steps"1. Force a primary failure and watch the fallback fire
Permalink to "1. Force a primary failure and watch the fallback fire"Temporarily corrupt the primary token (change one character) and reload with DevTools open. You should see the primary blocked with net::ERR_SRI_FAILED, then a second request to the fallback origin returning 200 OK.
lib.min.js (cdn-a) net::ERR_SRI_FAILED
lib.min.js (cdn-b) 200 OK
2. Confirm both origins serve a hash-matching file
Permalink to "2. Confirm both origins serve a hash-matching file"for host in cdn-a cdn-b; do
printf '%s: sha384-' "$host"
curl -sL --compressed "https://$host.example.com/lib.min.js" \
| openssl dgst -sha384 -binary | openssl base64 -A
echo
done
Both lines must print the same token for a shared-token setup. If they differ, switch to distinct per-origin tokens.
3. Confirm the fallback carries crossorigin
Permalink to "3. Confirm the fallback carries crossorigin"# Inspect the rendered DOM after failover in the console
document.querySelectorAll('script[src*="cdn-b"]').forEach(s =>
console.log(s.src, s.integrity, s.crossOrigin));
Expected output shows a non-empty integrity and crossOrigin === "anonymous" for the injected fallback tag.
Related
Permalink to "Related"- Handling SRI Failures with onerror Handlers — the browser-side mechanics of when
onerrorfires and how to build robust failure handlers - Graceful Fallback Strategies — parent page covering the full range of SRI recovery patterns
- CDN Trust Mapping & Routing — keeping every failover origin inside an explicit trust boundary so a fallback never routes to an unverified source