Handling SRI Failures with onerror Handlers
Permalink to "Handling SRI Failures with onerror Handlers" This page is part of Graceful Fallback Strategies, which sits within the Core SRI Fundamentals & Browser Security Boundaries section. It covers exactly one topic: how to intercept a browser-blocked SRI validation failure, inject a verified replacement asset, and do so without breaking your Content Security Policy or creating infinite retry loops.
Quick Reference
Permalink to "Quick Reference"| Attribute / property | Required value | Notes |
|---|---|---|
integrity on primary <script> |
sha384-<base64> |
SHA-384 is the industry default; SHA-512 for high-assurance |
crossorigin on primary <script> |
anonymous |
Mandatory on cross-origin resources; omitting it blocks SRI |
onerror attachment method |
addEventListener('error', fn) |
Avoid inline onerror="" — requires unsafe-inline in CSP |
Fallback src |
Self-hosted path (/assets/…) |
Eliminates need for a second integrity check |
| State guard | window.__sriFallbackTriggered |
Prevents infinite injection loops |
| Error event bubbles? | No | Must capture directly on element or via document capture phase |
Browser support: SRI is supported in Chrome 45+, Firefox 43+, Safari 11.1+, and Edge 17+. The onerror element event has universal support across all of these.
Why onerror Handlers Exist in an SRI Context
Permalink to "Why onerror Handlers Exist in an SRI Context" When an SRI validation fails the browser aborts resource loading before any script bytes execute, then dispatches an error event on the originating element. Without an onerror handler your application silently loses the dependency — no console recovery, no fallback, no user notification.
The onerror handler gives you a deterministic hook to substitute a verified local copy of the asset before application logic tries to call functions that no longer exist. This matters because SRI failures are not always attacks: a CDN cache-buster that rotates assets without updating your integrity attribute causes the same blocked-resource event as a genuine supply chain compromise. Your fallback path handles both causes identically, while your telemetry separates them.
The trust boundary is strict: onerror fires after the cryptographic decision has been made. You are not bypassing integrity enforcement — you are responding to it. The browser’s hash comparison, described in detail at Browser Enforcement & Security Boundaries, is complete and irrevocable by the time onerror runs.
SRI Failure Lifecycle Diagram
Permalink to "SRI Failure Lifecycle Diagram"Canonical Implementation Example
Permalink to "Canonical Implementation Example"The complete production pattern below uses programmatic event binding, a state guard, violation telemetry, and a local fallback — all without inline event attributes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- State guard initialised before any script tags -->
<script>window.__sriFallback = {};</script>
</head>
<body>
<!-- Primary CDN script with SRI -->
<script
id="vendor-lib"
src="https://cdn.vendor.com/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<script>
(function () {
var el = document.getElementById('vendor-lib');
if (!el) return;
el.addEventListener('error', function handleSriFailure(evt) {
var scriptId = evt.target.id || evt.target.src;
// State guard — prevents infinite loop if the fallback also fails
if (window.__sriFallback[scriptId]) return;
window.__sriFallback[scriptId] = true;
// Telemetry — fire-and-forget before starting fallback load
fetch('/api/sri-violation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'sri-onerror',
src: evt.target.src,
timestamp: Date.now()
})
}).catch(function () {});
// Inject verified local fallback
var fallback = document.createElement('script');
fallback.src = '/assets/vendor/lib.min.js'; // same-origin — no integrity attr needed
fallback.onerror = function () {
console.error('[SRI] Both primary and fallback failed for', scriptId);
};
document.head.appendChild(fallback);
});
}());
</script>
</body>
</html>
Key points to notice:
crossorigin="anonymous"is present on the primary element. Without it the browser makes a no-CORS request, receives an opaque response whose body cannot be read for hash comparison, and blocks the resource beforeonerroreven fires.- The state guard uses the element’s
idas a key so the same pattern scales to multiple scripts on the same page without a collision. - Telemetry is sent before the fallback loads, not after, to ensure capture even if the fallback itself fails. The
/api/sri-violationendpoint is deliberately separate from your CSPreport-uri, but the two feeds should land in the same alerting pipeline — see Alerting on SRI Failures from CSP Reports for the correlation rules. - The local fallback path begins with
/assets/…(same origin). Same-origin resources do not need anintegrityattribute because the browser’s SRI enforcement only applies to cross-origin fetches. Keeping that copy byte-identical to the CDN build is the hard part; Serving Local Fallback Bundles covers the vendoring and cache-header side of it.
Variant Examples
Permalink to "Variant Examples"Variant 1 — Promise-based loader for dynamic injection
Permalink to "Variant 1 — Promise-based loader for dynamic injection"When scripts are loaded programmatically rather than parsed from HTML, wrap the pattern in a Promise for clean async/await composition:
function loadWithSriFallback(primaryUrl, integrityHash, fallbackUrl) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = primaryUrl;
script.integrity = integrityHash;
script.crossOrigin = 'anonymous';
script.onload = resolve;
script.onerror = function () {
// Telemetry
navigator.sendBeacon('/api/sri-violation', JSON.stringify({
type: 'sri-onerror',
src: primaryUrl
}));
// Fallback
var fb = document.createElement('script');
fb.src = fallbackUrl;
fb.onload = resolve;
fb.onerror = function () {
reject(new Error('SRI fallback failed: ' + fallbackUrl));
};
document.head.appendChild(fb);
};
document.head.appendChild(script);
});
}
// Usage
loadWithSriFallback(
'https://cdn.vendor.com/react.production.min.js',
'sha384-GLdf3rnORLAB3Vx71uM2PF6cda1PFt/6LFrO9nO1xsOLMQXa3gC1ParIF28EQIM',
'/assets/vendor/react.production.min.js'
).then(function () {
console.log('React loaded (primary or fallback)');
}).catch(function (err) {
console.error('Critical load failure:', err);
});
navigator.sendBeacon is preferred here over fetch because it guarantees delivery even if the page is navigated away immediately after the error, and it does not block the fallback injection.
Variant 2 — Document-level capture for multiple scripts
Permalink to "Variant 2 — Document-level capture for multiple scripts"For pages with many third-party scripts, a single capture-phase listener on document handles all of them without per-element setup:
document.addEventListener('error', function (evt) {
var el = evt.target;
// Only handle <script> and <link rel="stylesheet"> SRI failures
if (el.tagName !== 'SCRIPT' && el.tagName !== 'LINK') return;
if (!el.integrity) return; // not an SRI-tagged element
var src = el.src || el.href;
if (window.__sriFallback && window.__sriFallback[src]) return;
window.__sriFallback = window.__sriFallback || {};
window.__sriFallback[src] = true;
var fallbackSrc = '/assets/fallbacks/' + src.split('/').pop();
var fallback = document.createElement(el.tagName);
if (el.tagName === 'SCRIPT') {
fallback.src = fallbackSrc;
} else {
fallback.rel = 'stylesheet';
fallback.href = fallbackSrc;
}
document.head.appendChild(fallback);
}, true); // capture phase — resource errors do not bubble
The true third argument switches to capture phase, which is required because resource-load errors do not bubble up the DOM. Without it, the listener on document never receives the event. The propagation path below is what makes this asymmetric: the capture phase walks down from document to the failing element, so a capturing listener sees the event on the way in, while the return leg that a normal listener depends on is never taken for resource errors.
One consequence worth planning for: a capture-phase listener on document also sees error events from images, iframes and media elements, which is why the guard filters on tagName and on the presence of an integrity property before doing anything. Without that filter, a broken avatar image would trigger a fallback injection for a script that never failed.
Variant 3 — CSP-nonce-compatible injection
Permalink to "Variant 3 — CSP-nonce-compatible injection"When your page uses a strict Configuring Content-Security-Policy with SRI policy with strict-dynamic and per-request nonces, injected scripts must carry the nonce:
function injectFallbackWithNonce(fallbackSrc) {
// Retrieve the nonce from an existing trusted script element
var nonceSource = document.querySelector('script[nonce]');
var nonce = nonceSource ? nonceSource.nonce : '';
var fallback = document.createElement('script');
fallback.src = fallbackSrc;
if (nonce) fallback.setAttribute('nonce', nonce);
document.head.appendChild(fallback);
}
With strict-dynamic, a script injected by a nonce-trusted script inherits trust automatically in modern browsers — but assigning the nonce explicitly preserves compatibility with browsers that do not yet implement the propagation rule.
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Missing
crossorigin="anonymous"causes a silent non-SRI block. The browser makes a no-CORS request, cannot read the opaque response body, and refuses the resource.onerrorfires but the failure is not an SRI mismatch — it is a CORS restriction. Always includecrossorigin="anonymous"on every cross-origin<script>and<link>with anintegrityattribute; How CORS and crossorigin Affect SRI sets out the response headers the origin has to send for that request mode to succeed. -
Inline
onerror=""requiresunsafe-inlinein CSP. Inline event handlers are treated as inline scripts. A strict CSP blocks them before they execute, meaning your fallback never fires. UseaddEventListenerattached from an external or nonce-trusted script block instead. -
Resource errors do not bubble. Attaching a non-capturing listener to
windowordocumentmisses element-levelerrorevents. Use the third argumenttrueondocument.addEventListeneror bind directly on the element. -
No state guard means infinite loops. If the fallback asset is also cross-origin with an
integrityattribute that fails,onerrorfires again for the fallback, which then injects another fallback, and so on until the call stack overflows. The guard shown in the canonical example prevents this. -
Fallback timing for render-blocking scripts. The
onerrorhandler runs asynchronously relative to script parsing. Scripts that are render-blocking (noasyncordefer) hold up the parser while the primary fetch completes. If the primary fails, your page hangs until the fallback loads. Useasyncon non-critical scripts to avoid layout shift and FOUC during fallback injection. -
onerrordoes not identify SRI failures specifically. HTTP 404s, network timeouts, and CORS rejections all trigger the sameerrorevent. To distinguish SRI failures from network failures, also listen forSecurityPolicyViolationEventondocumentand correlate byblockedURI.
The handler receives an Event, not an ErrorEvent: there is no message, no error and no status code on it, only target. Everything you can use to tell the causes apart lives outside the handler, in the developer tools or in a separate reporting stream. The matrix below lists the four causes that reach the same handler and the only signals that separate them.
Because the last column never varies, treat the handler as a recovery mechanism and never as a diagnosis. Record evt.target.src and let the server decide what happened: if the same URL is reported by thousands of clients within a minute it is a CDN or deployment problem, whereas a handful of reports from a single network path is a much stronger signal of interception.
Verification Steps
Permalink to "Verification Steps"Chrome DevTools — confirm the failure type
Open the Network panel. A blocked SRI resource shows status (blocked:integrity) in the Status column. The Console logs:
Failed to find a valid digest in the 'integrity' attribute
for resource 'https://cdn.vendor.com/lib.min.js'
with computed SHA-384 integrity 'sha384-ACTUAL_HASH_HERE'.
The resource has been blocked.
If you see this message and then see your fallback URL load successfully in the Network panel, the handler is working correctly.
Firefox DevTools — confirm the failure type
Firefox logs:
None of the "sha384" hashes in the integrity attribute match
the content of the subresource.
The request appears in the Network panel with a red status indicator.
Automated test with Playwright
import { test, expect } from '@playwright/test';
test('SRI onerror fallback loads local asset', async ({ page }) => {
const sriViolations = [];
const fallbackLoads = [];
page.on('console', msg => {
if (msg.text().includes('sri-onerror')) sriViolations.push(msg.text());
});
page.on('request', req => {
if (req.url().includes('/assets/vendor/')) fallbackLoads.push(req.url());
});
// Intercept primary CDN and return body with wrong hash
await page.route('https://cdn.vendor.com/lib.min.js', route =>
route.fulfill({ body: '/* tampered */', contentType: 'application/javascript' })
);
await page.goto('http://localhost:3000/');
// Verify telemetry was sent and fallback was requested
expect(fallbackLoads.length).toBeGreaterThan(0);
});
Verify CSP alignment with a report-only header
Add Content-Security-Policy-Report-Only: script-src 'self' https://cdn.vendor.com; report-uri /csp-report during staging. Any fallback injection from an origin not in script-src generates a report before you promote to enforcement. For the full CSP and dynamic injection interaction, see the Dynamic Script Loading Patterns discussion.
Related
Permalink to "Related"- Graceful Fallback Strategies — the parent page covering multi-origin failover architecture, Nginx routing, and CI/CD parity gating
- Browser Enforcement & Security Boundaries — how the browser fetch pipeline enforces SRI before
onerrorfires - Configuring Content-Security-Policy with SRI — aligning
script-src, nonces, andstrict-dynamicwith dynamic fallback injection - Implementing Dynamic Script Loaders with Integrity — applying integrity checks to scripts loaded entirely at runtime
- Serving Local Fallback Bundles — vendoring a byte-identical copy of the CDN asset so the fallback this handler injects is worth trusting