Follow-up to the iframe embedding notes — one thing I glossed over there: WebView is not an iframe. It doesn’t just have different header rules, it doesn’t have a window.parent to send a message to at all. Worth its own note.
Why postMessage goes nowhere
window.postMessage() sends a message to another window — parent, opener, a child iframe’s contentWindow. Those references exist because the browser defines a hierarchy of windows and frames.
A native WebView host isn’t part of that hierarchy. The Android or iOS app embedding the WebView is a native process running a rendering engine, not a JS window object. So a page loaded inside a WebView calling window.parent.postMessage(token, "*") doesn’t error — it just has no listener on the other end. The message is sent into a hierarchy of one.
What actually bridges the gap
addJavascriptInterface(Object, name) — the simple, long-standing option on Android. The app exposes a Java/Kotlin object to the page as window.<name>, and the page calls it directly as a function: window.Android.onToken(token). It’s a real reflection call into native code, invoking whichever method the page names.
Methods must be annotated @JavascriptInterface (required since API 17, closing an older vulnerability where any public method was callable). Beyond that annotation, there’s no origin check — any page the WebView loads can call the bridge. That’s fine when the WebView only ever loads content you control. It’s a real risk the moment the WebView can navigate to a third-party or attacker-influenced URL, since it hands that page the ability to invoke arbitrary annotated native methods.
AndroidX WebMessageListener (WebViewCompat.postWebMessage / addWebMessageListener) — the newer replacement, and the one that actually implements standard postMessage/MessageChannel semantics between web content and native code. From the page’s side, it looks and behaves like real window.postMessage — because it is real postMessage, just delivered to a native listener instead of another window. The listener is registered with a set of allowed origins, so an unrelated page loaded into the same WebView can’t reach it. It depends on the installed WebView (Chromium) version on the device, so gate it behind WebViewFeature.isFeatureSupported(WEB_MESSAGE_LISTENER) before relying on it.
iOS has the same conceptual split: window.webkit.messageHandlers.<name>.postMessage(data) via WKScriptMessageHandler. It’s named postMessage, and reads like the web API, but it’s WKWebView’s own bridge — not cross-window messaging.
The rule of thumb
- Parent context is a browser window or iframe → real
window.postMessage/window.opener.postMessageworks as expected. - Parent context is a native WebView host → real
postMessagedoesn’t cross the boundary. UseaddJavascriptInterfacefor something quick on content you fully control, or the AndroidXWebMessageListenerwhen the payload matters — auth tokens, session data, anything you wouldn’t want a stray page to be able to call into.
Same shape as the iframe case: figure out which container you’re actually in before picking the messaging mechanism, because the two look similar from the JS side and fail differently when you guess wrong.