When a page inside your app (e.g., a login form in a WebView) needs to send a custom header with each request, the simplest approach is to expose a tiny native function to JavaScript—then call it just-in-time and attach the value to requests.
This guide shows a modern, safe bridge:
- Android (Java): Jetpack WebKit
addWebMessageListener
(origin-restricted) (Android Developers) - iOS (Swift):
WKScriptMessageHandler
with a small promise-style callback
We’ll call the native function getCustomHeaderValue()
, and we’ll send it as X-Custom-Header
.
Why this pattern?
- Control: Attach the header on exactly the requests you choose (using
fetch()
/XHR). - Safety: On Android,
WebMessageListener
lets you allowlist origins and reply via a one-shot reply proxy—safer than exposing a full Java object. (android.googlesource.com) - Simplicity: The page just calls a function; native returns a string.
Android (Java) — using addWebMessageListener
(recommended)
Add AndroidX WebKit:
dependencies { implementation "androidx.webkit:webkit:1.9.0" }
Activity (Java):
// MainActivity.java (excerpt) import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; import androidx.webkit.WebMessageCompat; import androidx.webkit.JavaScriptReplyProxy; final String JS_OBJECT_NAME = "customHeader"; // window.customHeader final String ALLOWED_ORIGIN = "https://your-domain.example"; if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, JS_OBJECT_NAME, java.util.Collections.singleton(ALLOWED_ORIGIN), (view, message, sourceOrigin, isMainFrame, replyProxy) -> { if (!isMainFrame) return; if (sourceOrigin == null || !ALLOWED_ORIGIN.equals(sourceOrigin.toString())) return; if ("getCustomHeaderValue".equals(message.getData())) { String header = CustomHeaderProvider.getCustomHeaderValue(); // your code if (header == null) header = ""; replyProxy.postMessage(WebMessageCompat.create(header)); } } ); }
This listener injects a JS object (
window.customHeader
) that canpostMessage(...)
to native, and native replies using theJavaScriptReplyProxy
. (androidx.de)
Page JS (works with the injected object):
<script> function getCustomHeaderValue() { return new Promise((resolve) => { if (!window.customHeader || typeof window.customHeader.postMessage !== 'function') { resolve(""); return; } // receive the one-shot reply from native: window.customHeader.onmessage = (event) => { resolve((event && event.data) ? String(event.data) : ""); window.customHeader.onmessage = null; }; window.customHeader.postMessage('getCustomHeaderValue'); }); } // Example: attach to login request document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); const header = await getCustomHeaderValue(); const body = Object.fromEntries(new FormData(e.target).entries()); await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Custom-Header': header }, body: JSON.stringify(body), credentials: 'include' }); }); </script>
Hardening tips
- Inject the listener only after verifying the page’s URL matches your allowlist.
- Disable mixed content; use HTTPS origins. (android.googlesource.com)
- Keep the native surface tiny: just
getCustomHeaderValue()
.
iOS (Swift) — WKScriptMessageHandler
Create a WKWebView
with a message handler named customHeader
:
class ViewController: UIViewController, WKScriptMessageHandler { private var webView: WKWebView! private let handlerName = "customHeader" override func viewDidLoad() { super.viewDidLoad() let cc = WKUserContentController() cc.add(self, name: handlerName) let config = WKWebViewConfiguration() config.userContentController = cc webView = WKWebView(frame: view.bounds, configuration: config) view.addSubview(webView) // load local index.html or your remote page } private func getCustomHeaderValue() -> String { // Your fast, cached implementation return "DEMO-CUSTOM-HEADER-VALUE-IOS" } func userContentController(_ uc: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == handlerName, let data = message.body as? String, data == "getCustomHeaderValue" else { return } let header = getCustomHeaderValue().replacingOccurrences(of: "\"", with: "\\\\\"") let js = "window.__resolveCustomHeader && window.__resolveCustomHeader(\"\(header)\");" webView.evaluateJavaScript(js, completionHandler: nil) } }
Page JS:
<script> function getCustomHeaderValue() { return new Promise((resolve) => { window.__resolveCustomHeader = (val) => { resolve(val || ""); delete window.__resolveCustomHeader; }; if (window.webkit?.messageHandlers?.customHeader) { window.webkit.messageHandlers.customHeader.postMessage('getCustomHeaderValue'); } else { resolve(""); } }); } </script>
Performance & UX
- Make
getCustomHeaderValue()
fast. If it can be slow/async, pre-compute and cache in memory; refresh in the background so the bridge returns instantly. - If you can’t switch from a traditional
<form>
submit, inject a hidden fieldcustomData
and set it before submit; the server can read it from the body.
QA checklist
- ✅ Header present on all sensitive requests (login, token refresh, etc.).
- ✅ Empty/invalid header paths tested (server rejects/logs correctly).
- ✅ Origin allowlist working (injected object absent on untrusted origins).
- ✅ No mixed content; HTTPS enforced.
Conclusion
By exposing a tiny, purpose-built Custom Header Method (getCustomHeaderValue()
) to your WebView and attaching its value with fetch()
as X-Custom-Header
, you get a clean, testable way to protect critical flows like login without over-exposing native capabilities. On Android, prefer the WebMessageListener approach with an origin allowlist; on iOS, use a minimal WKScriptMessageHandler—both keep the surface area small and the data flow explicit. Pre-warm or cache the header value so calls feel instant, and keep your WebView hardened (HTTPS only, no mixed content). If you must keep traditional form posts, fall back to a hidden field. With this pattern in place, you can extend the same header strategy to any sensitive API call across your app.
Written By

I’m an Enterprise Architect at Akamai Technologies with over 14 years of experience in mobile app development across iOS, Android, Flutter, and cross-platform frameworks. I’ve built and launched 45+ apps on the App Store and Play Store, working with technologies like AR/VR, OTT, and IoT.
My core strengths include solution architecture, backend integration, cloud computing, CDN, CI/CD, and mobile security, including Frida-based pentesting and vulnerability analysis.
In the AI/ML space, I’ve worked on recommendation systems, NLP, LLM fine-tuning, and RAG-based applications. I’m currently focused on Agentic AI frameworks like LangGraph, LangChain, MCP and multi-agent LLMs to automate tasks