← Debugging Guides

阅读中文版本

postMessage Silently Ignored — Origin Mismatch in iframes

Debug postMessage messages that never arrive, where targetOrigin and event.origin disagree, verify both sides of the handshake, and validate strictly.

intermediatedebuggingbrowseriframepostmessage

Symptoms

  • postMessage calls from parent to iframe (or back) produce no message event and no error
  • The handshake works on the main domain but silently fails on preview domains or subdomains
  • Message events fire with unexpected data or from unexpected origins after a domain change

Possible causes

  • targetOrigin passed as '*' or the wrong origin, so the browser silently drops the message when origins do not match
  • The receiver checks event.origin against a stale allowlist and ignores valid messages
  • One side moved to a new domain or scheme (http to https, www to apex) and the other kept the old origin
  • The iframe navigated internally before the message was posted, so no listener existed yet

Diagnosis steps

Log both sides of every exchange

sender: console.log('post', targetOrigin, data); receiver: console.log('msg', event.origin, event.data)

Interpretation

A post logged without a matching message event means the browser dropped it; the origin pair in the logs names the mismatch.

Verify the exact origins in play

console.log(location.origin) in both parent and iframe contexts

Interpretation

Scheme, host, and port must match exactly; a trailing slash or www mismatch is enough to drop a message silently.

Confirm listener timing

log listener registration; post after iframe load event

Interpretation

A message posted before the iframe registered its listener vanishes even with correct origins — the race is a variant.

Root cause

postMessage with a specific targetOrigin delivers only if the recipient window document origin matches it exactly; otherwise the browser discards the message without any error. On the receiving side, event.origin reflects the sender actual origin, and strict validation silently ignores anything outside the allowlist. Both sides must agree on the exact origin strings.

Fix

Pass the exact intended targetOrigin instead of wildcards, validate event.origin against an explicit allowlist on every receiver, sync the allowlist through configuration when domains change, and sequence the handshake so listeners exist before first post.

Risk note

Wildcard targetOrigin leaks message content to whatever document occupies the frame at delivery time. Exact origins are the security contract.

Verify the fix

A bidirectional handshake test on every domain variant (apex, www, preview) exchanges messages with correct origin validation; tampered origins are rejected.

Symptom walkthrough

The silence is the bug. The sender logs the post; the receiver logs nothing; no exception anywhere:

iframe.contentWindow.postMessage({ type: 'init' }, '*');
// -> no message event in the iframe, no error in either console

The mechanism: with a concrete targetOrigin, the browser compares it against the recipient document origin at delivery time and drops the message if they differ — silently, by specification. The receiving half has its own trap: the handler validates event.origin against an allowlist, and a stale list (old domain, http scheme, www versus apex) ignores valid senders just as quietly. Log both sides of every exchange and compare the origin strings character by character.

Fix in practice

Exact origins on the way out:

iframe.contentWindow.postMessage(
  { type: 'init', token },
  'https://widget.example.com'    // never '*' for real payloads
);

Strict allowlist on the way in:

const ALLOWED = new Set([
  'https://app.example.com',
  'https://preview.example.com',  // kept in sync via config, not hardcoded twice
]);

window.addEventListener('message', (event) => {
  if (!ALLOWED.has(event.origin)) {
    console.warn('rejected message from', event.origin);
    return;
  }
  handle(event.data);
});

And sequence the handshake: the parent posts init only after the iframe load event, and the iframe posts ready immediately after registering its listener — closing the race where an early message finds no listener and vanishes.