The Browser Blocked It. window.onerror Never Fired.
I put a Content Security Policy of img-src 'none' on a page in Chrome this afternoon, pointed an <img> at a host it forbids, and watched what the page could tell me about it. Here is everything that happened: // secu
I put a Content Security Policy of img-src 'none' on a page in Chrome this afternoon, pointed an <img> at a host it forbids, and watched what the page could tell me about it.
Here is everything that happened:
// securitypolicyviolation fired once, with the detail you would want:
{
blockedURI: "https://example.com/blocked-by-policy.png",
violatedDirective: "img-src",
effectiveDirective:"img-src",
disposition: "enforce"
}
window.onerror fired: 0 times
unhandled rejections: 0
And on a second run, with a handler on the element itself:
img.onerror fired: 1
img.onload fired: 0
window.onerror fired: 0
Three things in there are worth more than the policy itself.
window.onerror is the one that does not fire
That is the global handler. It is what error trackers hook, because it is the only way to catch things nobody wrapped in a try. A CSP violation does not go through it, because a violation is not a JavaScript error: nothing throws, nothing rejects, and execution does not stop. MDN says this outright, and the zero above is the same statement with a number attached.
So a page can have half its scripts, images or styles blocked by policy and your tracker stays empty. Not quiet because nothing broke. Quiet because this kind of broken does not raise anything it listens for.
The element handler fires, and tells you nothing
img.onerror did fire. That is worth knowing, because it means the failure is catchable if you happened to put a handler on that element. But look at what it gives you: an error event, with no reason attached. The same handler fires for a 404, a DNS failure, a timeout and a policy block, and from inside it you cannot tell which you got.
The securitypolicyviolation event is the only one that names the directive and the blocked URI. That is the difference between "an image failed" and "our policy forbade this host".
The part that caught me out
The blocked image reported complete: true.
Its naturalWidth was 0, so the pixels never arrived, but complete is the property most code reaches for when it wants to know whether an image finished. The HTML Standard is clear about why: complete is true when the request's state is "broken" and there is no pending request. Broken counts as finished. So anything using it as a proxy for success gets a confident yes about a resource that was never allowed to load.
Where this actually bites
If the policy were only ever yours, you would find this in development. It often is not.
Corporate proxies and endpoint security products inject or tighten CSP headers on the way through. A policy can name a CDN hostname that differs by region. Staging commonly runs report-only while production enforces, which is the same policy in two dispositions and only one of them breaks anything. In all of those, the page is blocked for them and fine for you, and your console is clean because you are not the one being filtered.
What arrives is "the button does nothing" or "the images are missing", which is accurate, and which you cannot reproduce.
What to do about it
Register for the event and send it somewhere you look:
document.addEventListener("securitypolicyviolation", (e) => {
report({
directive: e.effectiveDirective,
blocked: e.blockedURI,
source: e.sourceFile,
line: e.lineNumber,
disposition: e.disposition, // "enforce" or "report"
});
});
It fires on Document, on the Element that caused it, and inside workers, so a listener on the document covers the common cases and a worker needs its own.
The broader point is the one I keep running into from different directions: an empty error tracker is not evidence that nothing went wrong. It is evidence that nothing went wrong in the one way the tracker was built to notice. A policy violation is a whole category of page-level failure that raises nothing, reports itself as complete, and waits for somebody to have registered for an event they have probably never heard of.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.