Bug Report
Capacitor Version
Plugin Version
@capgo/capacitor-inappbrowser: 8.6.16
context(s)
ManualModel: false
AutoMode: false
CapgoCloud: false
OnPremise: false
Platform(s)
Android (the JS bridge injection lives in android/src/main/assets/proxy-bridge.js and is gated by ProxyRequestSupport.shouldInjectBridge — iOS uses WKURLSchemeHandler and is not affected).
Current Behavior
When openWebView is called with the new-API outboundProxyRules and/or inboundProxyRules (and no legacy proxyRequests / proxyRequestsPattern), the proxy bridge script is still injected into every page load and silently proxies every page-level fetch / XMLHttpRequest / form submission through the /_capgo_proxy_?u=…&rid=… marker URL, regardless of whether the URL matches any rule.
Trace:
-
WebViewDialog#onCreate enables the bridge whenever _options.shouldEnableNativeProxy() is true (i.e., as soon as you set any outbound or inbound rule):
if (_options.shouldEnableNativeProxy()) {
proxyAccessToken = UUID.randomUUID().toString();
proxyBridge = new ProxyBridge(proxyAccessToken);
_webView.addJavascriptInterface(proxyBridge, "__capgoProxy");
proxyBridgeScript = loadProxyBridgeScript();
}
-
ProxyRequestSupport.shouldInjectBridge returns true for the same condition, so the bridge is re-injected on every onPageStarted:
static boolean shouldInjectBridge(Options options) {
return options != null && options.shouldEnableNativeProxy();
}
-
prepareProxyBridgeScript only populates the URL pattern for legacy mode, so for the new-API path the placeholder is replaced with an empty string:
String proxyRegexSource = "";
if (ProxyRequestSupport.usesLegacyJsProxyMode(_options) && _options.getProxyRequestsPattern() != null) {
proxyRegexSource = _options.getProxyRequestsPattern().pattern();
}
-
In proxy-bridge.js, an empty string produces proxyRequestPattern = null, and shouldProxyBridgeUrl treats a falsy pattern as "match everything":
const proxyRegexSource = "___CAPGO_PROXY_REGEX___"; // ""
let proxyRequestPattern = null;
if (proxyRegexSource) { /* skipped */ }
// ...
return !urlRegex || urlRegex.test(resolvedUrl); // !null === true
Net effect: every JS-initiated request on every page in the webview is rewritten to the marker URL, stored in ProxyBridge, intercepted natively, then replayed through performNativeRequest. This happens even for URLs that match no outbound/inbound rule — they were supposed to be handled directly by the WebView. Side effects:
window.fetch / XMLHttpRequest.prototype.{open,send,setRequestHeader,…} are monkey-patched on origins the developer never asked to touch (trivially detectable by bot/anti-automation checks, e.g. Facebook).
- Every request pays an extra IPC + native HTTP replay round trip, losing WebView caching and any Service Worker.
- Form
submit is rewritten to a GET navigation to the marker URL, which can break POST forms.
- Bodies are forced through base64 encoding; large multipart bodies are needlessly buffered.
This is reproducible with the exact snippet that the README documents as the canonical way to use delegateToJs (see "Stub one script from JavaScript" example): inbound-rules-only with a single connect.facebook.net rule causes the bridge to be injected into the Grailed signup page and wrap every fetch/XHR.
Expected Behavior
The JS bridge (proxy-bridge.js) only has a real effect in legacy mode — it's the only mode that actually puts a regex source into proxyRegexSource. In the new outbound/inbound rules mode, native-side shouldInterceptRequest already sees every WebView request and runs the rule matching itself; the JS bridge is redundant and misbehaving (no filter → wraps everything).
shouldInjectBridge should mirror usesLegacyJsProxyMode, not shouldEnableNativeProxy:
static boolean shouldInjectBridge(Options options) {
- return options != null && options.shouldEnableNativeProxy();
+ return options != null && usesLegacyJsProxyMode(options);
}
After that change:
- Legacy
proxyRequests: true / proxyRequests: "<regex>" keep working exactly as today (their tests should be unaffected).
- New-API users get rule-based native interception only, with no global
fetch/XHR monkey-patching and no per-fetch marker round trip.
Code Reproduction
Minimal repro using the library's own README example:
import { InAppBrowser, addProxyHandler } from '@capgo/capacitor-inappbrowser';
const proxyHandle = await addProxyHandler(async (request) => {
if (request.phase === 'inbound' && request.url.includes('connect.facebook.net')) {
return new Response('window.FB = {};', {
status: 200,
headers: { 'Content-Type': 'application/javascript; charset=utf-8' },
});
}
return null;
});
await InAppBrowser.openWebView({
url: 'https://example.com/',
inboundProxyRules: [
{ urlRegex: '^https://connect\\.facebook\\.net/.*', action: 'delegateToJs' },
],
});
// On Android, after the page loads, run in the WebView console:
// fetch('https://example.com/anything')
// Observe in logcat (tag `InAppBrowserProxy`) that the request was routed via
// `/_capgo_proxy_?u=...&rid=...` and replayed through `performNativeRequest`,
// even though no rule matches `example.com/anything`.
// Also: `window.fetch.toString()` returns the wrapped (non-native) implementation.
Other Technical Details
npm --version output:
node --version output:
pod --version output (iOS issues only): n/a — Android-only bug.
Capacitor: @capacitor/core@^8.3.1, @capacitor/android@^8.3.1.
Additional Context
Suggested one-line fix (also tested locally as a patch):
--- a/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyRequestSupport.java
+++ b/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyRequestSupport.java
@@
static boolean shouldInjectBridge(Options options) {
- return options != null && options.shouldEnableNativeProxy();
+ return options != null && usesLegacyJsProxyMode(options);
}
Rationale: prepareProxyBridgeScript already conditions proxyRegexSource on usesLegacyJsProxyMode, so anything outside legacy mode runs the bridge with a null pattern — clearly unintended. After this change:
ProxyBridge/__capgoProxy JavaScript interface is still wired up by shouldEnableNativeProxy (cheap, no script injection).
- The actual
evaluateJavascript(proxyBridgeScript, ...) in onPageStarted only fires when there's a real legacy regex to apply.
- The new-rules path is entirely native-side via
shouldInterceptRequest, which already handles everything correctly through shouldHandleNonBridgeRequest → outbound/inbound rule matching → performNativeRequest.
Happy to open a PR if useful. A regression test would need to drive a real WebView page (not just NativeRequestContext unit tests) so it sees window.fetch.toString() being mutated — likely why this hasn't been caught.
Bug Report
Capacitor Version
Plugin Version
context(s)
Platform(s)
Android (the JS bridge injection lives in
android/src/main/assets/proxy-bridge.jsand is gated byProxyRequestSupport.shouldInjectBridge— iOS usesWKURLSchemeHandlerand is not affected).Current Behavior
When
openWebViewis called with the new-APIoutboundProxyRulesand/orinboundProxyRules(and no legacyproxyRequests/proxyRequestsPattern), the proxy bridge script is still injected into every page load and silently proxies every page-levelfetch/XMLHttpRequest/ form submission through the/_capgo_proxy_?u=…&rid=…marker URL, regardless of whether the URL matches any rule.Trace:
WebViewDialog#onCreateenables the bridge whenever_options.shouldEnableNativeProxy()is true (i.e., as soon as you set any outbound or inbound rule):ProxyRequestSupport.shouldInjectBridgereturns true for the same condition, so the bridge is re-injected on everyonPageStarted:prepareProxyBridgeScriptonly populates the URL pattern for legacy mode, so for the new-API path the placeholder is replaced with an empty string:In
proxy-bridge.js, an empty string producesproxyRequestPattern = null, andshouldProxyBridgeUrltreats a falsy pattern as "match everything":Net effect: every JS-initiated request on every page in the webview is rewritten to the marker URL, stored in
ProxyBridge, intercepted natively, then replayed throughperformNativeRequest. This happens even for URLs that match no outbound/inbound rule — they were supposed to be handled directly by the WebView. Side effects:window.fetch/XMLHttpRequest.prototype.{open,send,setRequestHeader,…}are monkey-patched on origins the developer never asked to touch (trivially detectable by bot/anti-automation checks, e.g. Facebook).submitis rewritten to aGETnavigation to the marker URL, which can break POST forms.This is reproducible with the exact snippet that the README documents as the canonical way to use
delegateToJs(see "Stub one script from JavaScript" example): inbound-rules-only with a singleconnect.facebook.netrule causes the bridge to be injected into the Grailed signup page and wrap every fetch/XHR.Expected Behavior
The JS bridge (
proxy-bridge.js) only has a real effect in legacy mode — it's the only mode that actually puts a regex source intoproxyRegexSource. In the new outbound/inbound rules mode, native-sideshouldInterceptRequestalready sees every WebView request and runs the rule matching itself; the JS bridge is redundant and misbehaving (no filter → wraps everything).shouldInjectBridgeshould mirrorusesLegacyJsProxyMode, notshouldEnableNativeProxy:static boolean shouldInjectBridge(Options options) { - return options != null && options.shouldEnableNativeProxy(); + return options != null && usesLegacyJsProxyMode(options); }After that change:
proxyRequests: true/proxyRequests: "<regex>"keep working exactly as today (their tests should be unaffected).fetch/XHRmonkey-patching and no per-fetch marker round trip.Code Reproduction
Minimal repro using the library's own README example:
Other Technical Details
npm --versionoutput:node --versionoutput:pod --versionoutput (iOS issues only): n/a — Android-only bug.Capacitor:
@capacitor/core@^8.3.1,@capacitor/android@^8.3.1.Additional Context
Suggested one-line fix (also tested locally as a patch):
Rationale:
prepareProxyBridgeScriptalready conditionsproxyRegexSourceonusesLegacyJsProxyMode, so anything outside legacy mode runs the bridge with a null pattern — clearly unintended. After this change:ProxyBridge/__capgoProxyJavaScript interface is still wired up byshouldEnableNativeProxy(cheap, no script injection).evaluateJavascript(proxyBridgeScript, ...)inonPageStartedonly fires when there's a real legacy regex to apply.shouldInterceptRequest, which already handles everything correctly throughshouldHandleNonBridgeRequest→ outbound/inbound rule matching →performNativeRequest.Happy to open a PR if useful. A regression test would need to drive a real WebView page (not just
NativeRequestContextunit tests) so it seeswindow.fetch.toString()being mutated — likely why this hasn't been caught.