Tu intègres un embed tiers. Tu mets une CSP un peu sérieuse. Et un jour, sans warning, ton code se retrouve à attendre un iframe.onload qui ne viendra jamais. Le piège, c’est que ce n’est pas forcément un “bug navigateur” visible. C’est juste une conséquence assez brutale d’un blocage CSP sur la navigation de l’iframe.
Je te propose un angle très prod : pourquoi ça arrive (côté spec), comment arrêter de dépendre de load comme d’un signal “fiable”, et comment rendre tes embeds résilients sans ouvrir ta CSP à la tronçonneuse.
Pourquoi une iframe bloquée par CSP peut ne jamais déclencher load
L’intuition classique, c’est : “si ça échoue, j’aurai au moins un event”. En pratique, quand la navigation de l’iframe est bloquée par la Content Security Policy (typiquement frame-src ou child-src selon les cas), tu peux te retrouver avec une navigation avortée très tôt, et donc aucun document “chargé” au sens où ton code l’attend.
Le point important (et contre-intuitif) mis en lumière dans les discussions WHATWG : une navigation bloquée par CSP peut se traduire par une réponse “nulle” dans l’algorithme de navigation. Et si la navigation n’aboutit pas à un document, l’event load n’a aucune raison d’être émis. Résultat : si tu as mis une promesse du style “resolve sur load”, tu peux créer toi-même ton deadlock.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Le symptôme en prod : un embed “fiable” qui freeze ton UI
Ce bug-là, tu le vois souvent sous forme de spinner infini. Ou pire : une modale qui ne s’ouvre jamais parce que tu “gates” l’affichage sur onload. Sur un site e-commerce, ça se transforme vite en paiement “qui ne marche pas”. Sur une app B2B, en support qui sature parce que “le calendrier ne s’affiche pas”.
Ce qui rend le truc pénible, c’est que le blocage dépend de la politique effective au moment du chargement. Donc tu peux l’avoir en prod mais pas en staging, l’avoir sur une route mais pas une autre, l’avoir après un A/B test, l’avoir uniquement sur certaines sous-apps qui posent une CSP plus stricte. Et toi tu restes là à “debugger l’iframe”, alors que c’est ton intégration qui a supposé que load était un contrat.
Un pattern d’intégration robuste : handshake ou timeout, mais jamais “attendre load”
Mon avis est simple : une iframe, surtout tierce, c’est un appel réseau déguisé en DOM. Tu ne “wait” pas un service externe sans timeout et sans plan B. Donc tu ne devrais pas le faire avec un embed.
Quand tu peux : un handshake explicite via postMessage
Si tu contrôles le contenu chargé dans l’iframe (même sur un autre domaine) ou si le fournisseur te permet de configurer un message “ready”, c’est le meilleur signal. Tu ne dépends pas d’un event implicite, tu dépends d’un protocole clair : “je suis prêt, voilà mon type, voilà mon instance”.
Côté parent, tu attends un message signé (origin attendu + format attendu). Côté iframe, tu envoies un ping à window.parent quand ton app est réellement initialisée (pas quand le HTML est juste arrivé).
type EmbedReadyMsg = { type: 'EMBED_READY'; id: string };
type WaitOptions = {
iframe: HTMLIFrameElement;
expectedOrigin: string;
id: string;
timeoutMs?: number;
};
export function waitForEmbedReady({ iframe, expectedOrigin, id, timeoutMs = 4000 }: WaitOptions) {
return new Promise<void>((resolve, reject) => {
let done = false;
const cleanup = () => {
window.removeEventListener('message', onMessage);
clearTimeout(timer);
};
const onMessage = (event: MessageEvent) => {
if (event.origin !== expectedOrigin) return;
const data = event.data as Partial<EmbedReadyMsg>;
if (data?.type !== 'EMBED_READY') return;
if (data?.id !== id) return;
if (done) return;
done = true;
cleanup();
resolve();
};
const timer = window.setTimeout(() => {
if (done) return;
done = true;
cleanup();
reject(new Error(`Embed not ready after ${timeoutMs}ms (id=${id}). Possible CSP/frame-src block, network issue, or provider outage.`));
}, timeoutMs);
window.addEventListener('message', onMessage);
// Optionnel : tu peux quand même poser un onload pour de la télémétrie,
// mais surtout pas comme “signal de readiness”.
iframe.addEventListener('load', () => {
// utile pour logs, pas pour resolve
}, { once: true });
});
}Ce pattern a un avantage énorme : même si la ressource charge “vite”, tu n’affiches pas un composant cassé. Tu attends un signal fonctionnel. Et si ça n’arrive pas, tu tombes sur un fallback maîtrisé.
Quand tu ne peux pas : timeout court + UI de repli (et pas un spinner éternel)
Pour un embed tiers “black box”, tu n’auras pas toujours de postMessage exploitable. Dans ce cas, tu assumes que tu ne sais pas si ça a chargé, et tu construis une UX qui ne te met pas dans le mur. Ça veut dire un timeout court (quelques secondes) et un plan B utile : bouton “Ouvrir dans un nouvel onglet”, message clair, lien de support, ou même une version dégradée.
Le point important : ton UI doit pouvoir continuer à vivre sans l’iframe. Ne gate pas toute une page, une modale, ou un flux critique derrière un event qui n’est pas garanti. Un embed qui ne s’affiche pas doit être un incident, pas un freeze.
Instrumenter le problème : capter les violations CSP au lieu de deviner
Si tu veux arrêter de “jouer à l’aveugle”, il te faut des signaux observables. D’abord côté navigateur : le parent peut recevoir un SecurityPolicyViolationEvent quand une directive CSP bloque un chargement. C’est très pratique pour logger « frame-src bloqué vers tel domaine » et corréler avec ton timeout embed.
Ensuite côté CSP reporting : en 2026, le meilleur investissement, c’est de configurer un endpoint de report. Tu peux tester en Content-Security-Policy-Report-Only avant de casser la prod, puis passer en enforce avec un niveau de confiance correct. Et surtout, tu obtiens des événements “hard” sur ce qui a été bloqué, pas des suppositions.
Attention quand même : les reports peuvent être bruités, et ça peut partir en volume si tu as des pages très vues. Donc tu filtres, tu échantillonnes, tu stockes un minimum exploitable, et tu ajoutes des tags qui t’aident à répondre à une question simple : « quel changement a déclenché cette vague de blocages iframe ? »
CSP et embeds : durcir sans casser, ça demande une vraie stratégie
Le réflexe “autoriser tout” est tentant quand un widget tombe en panne. C’est aussi la façon la plus rapide de transformer une CSP en décoration. Ce que je fais sur des produits qui embed du tiers : une allowlist stricte, domaine par domaine, et je sépare bien les environnements. Si tu as un provider qui utilise plusieurs sous-domaines ou des redirects, tu le découvres en report-only, pas après un vendredi soir en prod.
Autre point terrain : évite de te reposer sur un seul domaine “marketing” du fournisseur. Beaucoup de SaaS servent l’embed depuis un domaine applicatif (ou un CDN) différent de leur site public. Et certains changent. Donc tu veux une relation contractuelle claire ou un mécanisme de “vendor allowlist” versionné côté config, pas un bout de header bricolé à la main.
Content-Security-Policy: default-src 'self';
frame-src 'self' https://pay.example-provider.com https://calendar.example-provider.com;
report-to csp-endpoint;
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://csp-report.example.com/report"}]}Et oui, je sais : tout le monde n’a pas envie d’opérer un endpoint de reports. Mais si tu dépends d’embeds critiques (paiement, identity, signature, calendrier), tu dépends déjà d’externes. Avoir les reports CSP, c’est juste accepter la réalité et se donner des preuves.
Les faux amis : ce qui ressemble à un blocage CSP mais ne l’est pas
Avant de partir en croisade contre CSP, pense aux autres causes fréquentes qui te donnent le même symptôme “iframe vide” et “pas de signal utile”. Tu as le X-Frame-Options et frame-ancestors côté serveur du fournisseur, qui peuvent interdire l’embarquement même si ta CSP est permissive. Tu as les blocages mixed content si tu charges une iframe HTTP dans une page HTTPS. Tu as des erreurs DNS, des timeouts réseau, des adblockers, des extensions privacy, ou un provider down.
La différence, c’est l’observabilité. Un blocage CSP est souvent visible via violation event et via reports. Un refus frame-ancestors ne se traite pas pareil et peut nécessiter de basculer sur un redirect top-level ou un flow OAuth “hors iframe”. Et un adblocker, tu ne le corriges pas : tu le contournes par une UX qui permet d’ouvrir ailleurs et de continuer.
Conclusion : traite l’iframe comme un service externe, pas comme du DOM
Le vrai piège, ce n’est pas que CSP puisse bloquer une iframe. C’est de croire que le navigateur te doit un load dans tous les scénarios. À partir du moment où tu acceptes que l’embed est une dépendance externe, tu changes ta façon d’intégrer : handshake quand tu peux, timeout + fallback quand tu ne peux pas, et de la télémétrie pour arrêter de diagnostiquer au feeling.
Et si tu dois retenir une règle simple : un spinner sans borne, sur un embed, c’est un bug de produit. Pas un détail d’implémentation.