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 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.
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.
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. -
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.
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