@capgo/capacitor-inappbrowser 8.7.5 → 8.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,8 @@ import java.util.Iterator;
50
50
  import java.util.List;
51
51
  import java.util.Map;
52
52
  import java.util.UUID;
53
+ import java.util.concurrent.ExecutorService;
54
+ import java.util.concurrent.Executors;
53
55
  import java.util.regex.Pattern;
54
56
  import java.util.regex.PatternSyntaxException;
55
57
  import org.json.JSONException;
@@ -66,6 +68,15 @@ import org.json.JSONObject;
66
68
  )
67
69
  public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.PermissionHandler {
68
70
 
71
+ private static final ExecutorService PROXY_RESPONSE_EXECUTOR = Executors.newFixedThreadPool(
72
+ Math.max(4, Runtime.getRuntime().availableProcessors()),
73
+ (runnable) -> {
74
+ Thread thread = new Thread(runnable, "CapgoInAppBrowser-ProxyResponse");
75
+ thread.setDaemon(true);
76
+ return thread;
77
+ }
78
+ );
79
+
69
80
  private final String pluginVersion = "8.6.17";
70
81
 
71
82
  private boolean resolveClientCertificatePrompt(PluginCall call) {
@@ -1684,8 +1695,10 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
1684
1695
  return;
1685
1696
  }
1686
1697
 
1687
- dialog.handleProxyResponse(requestId, decision);
1688
- call.resolve();
1698
+ PROXY_RESPONSE_EXECUTOR.execute(() -> {
1699
+ dialog.handleProxyResponse(requestId, decision);
1700
+ call.resolve();
1701
+ });
1689
1702
  }
1690
1703
 
1691
1704
  @PluginMethod
@@ -58,11 +58,10 @@ public class ProxyBridge {
58
58
  if (requestId == null) {
59
59
  return null;
60
60
  }
61
- StoredRequest storedRequest = storedRequests.remove(requestId);
62
- if (storedRequest != null) {
63
- storedRequestOrder.remove(requestId);
64
- }
65
- return storedRequest;
61
+ // Do not scan storedRequestOrder here: ConcurrentLinkedQueue.remove is O(n) and
62
+ // serializes parallel bridge-backed proxy requests. Stale queue ids are purged lazily
63
+ // in cleanupExpiredRequests().
64
+ return storedRequests.remove(requestId);
66
65
  }
67
66
 
68
67
  private void cleanupExpiredRequests(long now) {
@@ -3,6 +3,7 @@ package ee.forgr.capacitor_inappbrowser;
3
3
  import com.getcapacitor.JSObject;
4
4
  import java.io.IOException;
5
5
  import java.net.URL;
6
+ import java.net.URLConnection;
6
7
  import java.nio.charset.StandardCharsets;
7
8
  import java.util.Base64;
8
9
  import java.util.HashMap;
@@ -22,6 +23,8 @@ final class ProxyRequestSupport {
22
23
 
23
24
  record ParsedResponseHeaders(Map<String, String> responseHeaders, List<String> cookieHeaders) {}
24
25
 
26
+ record ProxiedResponseMetadata(String contentType, Map<String, String> headers) {}
27
+
25
28
  static final int SYNTHETIC_NATIVE_FAILURE_STATUS = 599;
26
29
  static final String SYNTHETIC_NATIVE_FAILURE_HEADER = "X-Capgo-Proxy-Error";
27
30
 
@@ -59,7 +62,7 @@ final class ProxyRequestSupport {
59
62
  private ProxyRequestSupport() {}
60
63
 
61
64
  static boolean shouldInjectBridge(Options options) {
62
- return options != null && options.shouldEnableNativeProxy();
65
+ return options != null && usesLegacyJsProxyMode(options);
63
66
  }
64
67
 
65
68
  static boolean usesLegacyJsProxyMode(Options options) {
@@ -109,6 +112,27 @@ final class ProxyRequestSupport {
109
112
  return !usesLegacyJsProxyMode(options) || matchesProxyRequestsPattern(options.getProxyRequestsPattern(), requestUrl);
110
113
  }
111
114
 
115
+ static boolean shouldBootstrapInitialProxyLoad(Options options) {
116
+ if (options == null || options.isPopupWindowMode()) {
117
+ return false;
118
+ }
119
+ String initialUrl = options.getUrl();
120
+ if (initialUrl == null || initialUrl.isBlank()) {
121
+ return false;
122
+ }
123
+ if (!options.shouldEnableNativeProxy()) {
124
+ return false;
125
+ }
126
+ if (shouldDelegateLegacyJsProxyRequest(options, initialUrl)) {
127
+ return true;
128
+ }
129
+ return shouldHandleNonBridgeRequest(options, initialUrl) && !usesLegacyJsProxyMode(options);
130
+ }
131
+
132
+ static boolean shouldReturnSyntheticNativeFailure(boolean bridgeBackedRequest, Options options, String requestUrl) {
133
+ return bridgeBackedRequest || shouldHandleNonBridgeRequest(options, requestUrl);
134
+ }
135
+
112
136
  static boolean isBridgeMarkerRequestUrl(String requestUrl) {
113
137
  if (requestUrl == null || requestUrl.isBlank()) {
114
138
  return false;
@@ -405,16 +429,164 @@ final class ProxyRequestSupport {
405
429
  return new WebResourceResponseMetadata(mimeType, encoding);
406
430
  }
407
431
 
432
+ static ProxiedResponseMetadata normalizeProxiedResponseMetadata(
433
+ String contentType,
434
+ Map<String, String> responseHeaders,
435
+ String requestUrl
436
+ ) {
437
+ Map<String, String> headers = responseHeaders != null ? new LinkedHashMap<>(responseHeaders) : new LinkedHashMap<>();
438
+ String headerContentType = findHeaderIgnoreCase(headers, "Content-Type");
439
+ String resolvedContentType = resolveProxiedContentType(firstNonEmpty(contentType, headerContentType), requestUrl);
440
+ if (resolvedContentType == null || resolvedContentType.isBlank()) {
441
+ resolvedContentType = "application/octet-stream";
442
+ }
443
+ if (!hasHeaderIgnoreCase(headers, "Content-Type")) {
444
+ headers.put("Content-Type", resolvedContentType);
445
+ }
446
+ return new ProxiedResponseMetadata(resolvedContentType, headers);
447
+ }
448
+
408
449
  static WebResourceResponseMetadata resolveWebResourceResponseConstructorMetadata(
409
450
  String contentType,
410
451
  Map<String, String> responseHeaders
411
452
  ) {
412
- if (hasHeaderIgnoreCase(responseHeaders, "Content-Type")) {
453
+ String headerContentType = findHeaderIgnoreCase(responseHeaders, "Content-Type");
454
+ if (headerContentType != null && shouldDeferConstructorMimeTypeToContentTypeHeader(headerContentType)) {
413
455
  return new WebResourceResponseMetadata(null, null);
414
456
  }
415
457
  return resolveWebResourceResponseMetadata(contentType, responseHeaders);
416
458
  }
417
459
 
460
+ static String resolveProxiedContentType(String contentType, String requestUrl) {
461
+ String mimeOnly = extractMimeType(contentType);
462
+ if (mimeOnly != null && !mimeOnly.isBlank() && !isGenericProxiedMimeType(mimeOnly)) {
463
+ if (shouldNormalizeSvgContentType(requestUrl, mimeOnly)) {
464
+ return "image/svg+xml";
465
+ }
466
+ return contentType.trim();
467
+ }
468
+
469
+ String guessedContentType = guessContentTypeFromRequestUrl(requestUrl);
470
+ if (guessedContentType != null) {
471
+ return guessedContentType;
472
+ }
473
+
474
+ return contentType != null && !contentType.isBlank() ? contentType.trim() : null;
475
+ }
476
+
477
+ static String guessContentTypeFromRequestUrl(String requestUrl) {
478
+ if (requestUrl == null || requestUrl.isBlank()) {
479
+ return null;
480
+ }
481
+
482
+ String fileName;
483
+ try {
484
+ fileName = new URL(requestUrl).getPath();
485
+ } catch (Exception error) {
486
+ return null;
487
+ }
488
+ if (fileName == null || fileName.isBlank()) {
489
+ return null;
490
+ }
491
+
492
+ int slashIndex = fileName.lastIndexOf('/');
493
+ if (slashIndex >= 0) {
494
+ fileName = fileName.substring(slashIndex + 1);
495
+ }
496
+ if (fileName.isBlank()) {
497
+ return null;
498
+ }
499
+
500
+ String guessedType = URLConnection.guessContentTypeFromName(fileName);
501
+ if (guessedType != null && !guessedType.isBlank()) {
502
+ return guessedType;
503
+ }
504
+
505
+ int dotIndex = fileName.lastIndexOf('.');
506
+ if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
507
+ return null;
508
+ }
509
+ String extension = fileName.substring(dotIndex + 1).toLowerCase(Locale.US);
510
+ return switch (extension) {
511
+ case "svg" -> "image/svg+xml";
512
+ case "png" -> "image/png";
513
+ case "jpg", "jpeg" -> "image/jpeg";
514
+ case "gif" -> "image/gif";
515
+ case "webp" -> "image/webp";
516
+ case "pdf" -> "application/pdf";
517
+ case "json" -> "application/json";
518
+ case "txt" -> "text/plain";
519
+ case "csv" -> "text/csv";
520
+ case "html", "htm" -> "text/html";
521
+ case "xhtml" -> "application/xhtml+xml";
522
+ default -> null;
523
+ };
524
+ }
525
+
526
+ private static boolean shouldNormalizeSvgContentType(String requestUrl, String mimeType) {
527
+ if (!"svg".equalsIgnoreCase(fileExtensionFromRequestUrl(requestUrl))) {
528
+ return false;
529
+ }
530
+ String normalizedMimeType = mimeType.toLowerCase(Locale.US);
531
+ return (
532
+ isGenericProxiedMimeType(normalizedMimeType) ||
533
+ "text/xml".equals(normalizedMimeType) ||
534
+ "application/xml".equals(normalizedMimeType)
535
+ );
536
+ }
537
+
538
+ private static String fileExtensionFromRequestUrl(String requestUrl) {
539
+ if (requestUrl == null || requestUrl.isBlank()) {
540
+ return "";
541
+ }
542
+ try {
543
+ String path = new URL(requestUrl).getPath();
544
+ if (path == null || path.isBlank()) {
545
+ return "";
546
+ }
547
+ int slashIndex = path.lastIndexOf('/');
548
+ String fileName = slashIndex >= 0 ? path.substring(slashIndex + 1) : path;
549
+ int dotIndex = fileName.lastIndexOf('.');
550
+ if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
551
+ return "";
552
+ }
553
+ return fileName.substring(dotIndex + 1);
554
+ } catch (Exception error) {
555
+ return "";
556
+ }
557
+ }
558
+
559
+ private static String extractMimeType(String contentType) {
560
+ if (contentType == null || contentType.isBlank()) {
561
+ return null;
562
+ }
563
+ String mimeType = contentType.split(";")[0].trim();
564
+ return mimeType.isEmpty() ? null : mimeType;
565
+ }
566
+
567
+ private static boolean isGenericProxiedMimeType(String mimeType) {
568
+ if (mimeType == null || mimeType.isBlank()) {
569
+ return true;
570
+ }
571
+ String normalizedMimeType = mimeType.trim().toLowerCase(Locale.US);
572
+ return (
573
+ normalizedMimeType.equals("application/octet-stream") ||
574
+ normalizedMimeType.equals("binary/octet-stream") ||
575
+ normalizedMimeType.equals("application/download") ||
576
+ normalizedMimeType.equals("application/x-download") ||
577
+ normalizedMimeType.equals("application/binary") ||
578
+ normalizedMimeType.equals("application/x-binary")
579
+ );
580
+ }
581
+
582
+ private static boolean shouldDeferConstructorMimeTypeToContentTypeHeader(String contentTypeHeader) {
583
+ String mimeType = extractMimeType(contentTypeHeader);
584
+ if (mimeType == null) {
585
+ return false;
586
+ }
587
+ return mimeType.toLowerCase(Locale.US).startsWith("multipart/");
588
+ }
589
+
418
590
  static ParsedResponseHeaders splitResponseHeaders(Map<String, List<String>> rawHeaders) {
419
591
  Map<String, String> responseHeaders = new HashMap<>();
420
592
  List<String> cookieHeaders = new java.util.ArrayList<>();
@@ -831,6 +1003,9 @@ final class ProxyRequestSupport {
831
1003
  return false;
832
1004
  }
833
1005
  String normalizedMimeType = mimeType.trim().toLowerCase(Locale.US);
1006
+ if (normalizedMimeType.startsWith("image/")) {
1007
+ return false;
1008
+ }
834
1009
  return (
835
1010
  normalizedMimeType.startsWith("text/") ||
836
1011
  normalizedMimeType.equals("application/json") ||
@@ -334,12 +334,13 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
334
334
  private boolean datePickerInjected = false; // Track if we've injected date picker fixes
335
335
  private final WebView capacitorWebView;
336
336
  private String instanceId = "";
337
- private final Map<String, ProxiedRequest> proxiedRequestsHashmap = new HashMap<>();
337
+ private final Map<String, ProxiedRequest> proxiedRequestsHashmap = new ConcurrentHashMap<>();
338
338
  private ProxyBridge proxyBridge;
339
339
  private String proxyBridgeScript;
340
340
  private String proxyAccessToken;
341
341
  private final ExecutorService executorService = Executors.newCachedThreadPool();
342
342
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
343
+ private final AtomicBoolean cookieFlushScheduled = new AtomicBoolean(false);
343
344
  private final Map<String, BlobDownloadSession> blobDownloadSessions = new ConcurrentHashMap<>();
344
345
  private final Map<String, ClientCertificateIdentity> clientCertificateIdentities = new ConcurrentHashMap<>();
345
346
  private int iconColor = Color.BLACK; // Default icon color
@@ -4906,7 +4907,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
4906
4907
  }
4907
4908
  String initiatorUrl = request.getRequestHeaders().get("Referer");
4908
4909
  if (initiatorUrl == null || initiatorUrl.isBlank()) {
4909
- initiatorUrl = getWebViewUrlOnMainThread(originalUrl);
4910
+ initiatorUrl = getWebViewUrlOrFallback(
4911
+ _options != null && _options.getUrl() != null ? _options.getUrl() : originalUrl
4912
+ );
4910
4913
  }
4911
4914
  String targetCookies = CookieManager.getInstance().getCookie(originalUrl);
4912
4915
  if (
@@ -5027,7 +5030,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5027
5030
  nativeResponse = performNativeRequest(requestContext);
5028
5031
  } catch (IOException error) {
5029
5032
  Log.e("InAppBrowserProxy", "Native request failed for: " + requestContext.url, error);
5030
- return bridgeBackedRequest ? createNativeFailureResponse(requestContext.url, error) : null;
5033
+ return createProxiedNativeFailureResponse(requestContext.url, bridgeBackedRequest, error);
5031
5034
  }
5032
5035
  }
5033
5036
 
@@ -5040,7 +5043,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5040
5043
  redirectReplay = followRedirectForWebView(requestContext, nativeResponse, redirectsFollowed);
5041
5044
  } catch (IOException error) {
5042
5045
  Log.e("InAppBrowserProxy", "Native redirect replay failed for: " + requestContext.url, error);
5043
- return bridgeBackedRequest ? createNativeFailureResponse(requestContext.url, error) : null;
5046
+ return createProxiedNativeFailureResponse(requestContext.url, bridgeBackedRequest, error);
5044
5047
  }
5045
5048
  if (redirectReplay != null) {
5046
5049
  requestContext = redirectReplay.requestContext;
@@ -5103,7 +5106,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5103
5106
  redirectReplay = followRedirectForWebView(requestContext, nativeResponse, redirectsFollowed);
5104
5107
  } catch (IOException error) {
5105
5108
  Log.e("InAppBrowserProxy", "Native redirect replay failed for: " + requestContext.url, error);
5106
- return bridgeBackedRequest ? createNativeFailureResponse(requestContext.url, error) : null;
5109
+ return createProxiedNativeFailureResponse(requestContext.url, bridgeBackedRequest, error);
5107
5110
  }
5108
5111
  if (redirectReplay != null) {
5109
5112
  requestContext = redirectReplay.requestContext;
@@ -5283,8 +5286,13 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5283
5286
  if (view == null || _webView == null) {
5284
5287
  return;
5285
5288
  }
5286
- _options.getCallbacks().pageLoadError();
5287
- if (request != null && request.isForMainFrame() && !isInitialized) {
5289
+ if (request == null || !request.isForMainFrame()) {
5290
+ return;
5291
+ }
5292
+ if (_options.getCallbacks() != null) {
5293
+ _options.getCallbacks().pageLoadError();
5294
+ }
5295
+ if (!isInitialized) {
5288
5296
  CharSequence description = error != null ? error.getDescription() : null;
5289
5297
  String message = description != null ? "Initial page load failed: " + description : "Initial page load failed";
5290
5298
  rejectOpenWebViewIfNeeded(message);
@@ -5538,9 +5546,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5538
5546
  shutdownExecutorServiceAsync();
5539
5547
 
5540
5548
  // Clear any remaining proxied requests
5541
- synchronized (proxiedRequestsHashmap) {
5542
- proxiedRequestsHashmap.clear();
5543
- }
5549
+ proxiedRequestsHashmap.clear();
5544
5550
 
5545
5551
  try {
5546
5552
  super.dismiss();
@@ -5768,10 +5774,29 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5768
5774
  }
5769
5775
 
5770
5776
  private boolean shouldBootstrapInitialLegacyProxyLoad() {
5771
- if (_options == null || _options.isPopupWindowMode()) {
5772
- return false;
5777
+ return ProxyRequestSupport.shouldBootstrapInitialProxyLoad(_options);
5778
+ }
5779
+
5780
+ private void loadInitialUrlDirect(Map<String, String> requestHeaders, String httpMethod, String httpBody) {
5781
+ if (_webView == null || _options == null) {
5782
+ return;
5783
+ }
5784
+
5785
+ Map<String, String> headers = requestHeaders != null ? requestHeaders : new HashMap<>();
5786
+ if (supportsRequestBody(httpMethod) && httpBody != null) {
5787
+ byte[] postData = httpBody.getBytes(StandardCharsets.UTF_8);
5788
+ _webView.postUrl(_options.getUrl(), postData);
5789
+ if (!headers.isEmpty()) {
5790
+ Log.w(
5791
+ "InAppBrowser",
5792
+ "Custom headers were provided but may not be sent with POST request. " +
5793
+ "Android WebView's postUrl method has limited header support."
5794
+ );
5795
+ }
5796
+ return;
5773
5797
  }
5774
- return ProxyRequestSupport.shouldDelegateLegacyJsProxyRequest(_options, _options.getUrl());
5798
+
5799
+ _webView.loadUrl(_options.getUrl(), headers);
5775
5800
  }
5776
5801
 
5777
5802
  private void loadInitialLegacyProxyContent(Map<String, String> requestHeaders, String httpMethod, String httpBody) {
@@ -5802,7 +5827,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5802
5827
 
5803
5828
  NativeResponseData responseData = proxyResult.responseData;
5804
5829
  if (responseData == null || !canBootstrapHtmlResponse(responseData.contentType)) {
5805
- rejectOpenWebViewIfNeeded("Initial legacy proxy request did not return bootstrap HTML");
5830
+ if (_webView == null) {
5831
+ return;
5832
+ }
5833
+ _webView.post(() -> loadInitialUrlDirect(initialHeaders, initialMethod, httpBody));
5806
5834
  return;
5807
5835
  }
5808
5836
 
@@ -5826,7 +5854,19 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5826
5854
  });
5827
5855
  } catch (IOException error) {
5828
5856
  Log.e("InAppBrowserProxy", "Initial legacy proxy bootstrap failed for: " + initialUrl, error);
5829
- rejectOpenWebViewIfNeeded("Initial legacy proxy bootstrap failed: " + error.getMessage());
5857
+ if (_webView == null) {
5858
+ return;
5859
+ }
5860
+ String failureBody = new String(
5861
+ ProxyRequestSupport.createNativeRequestFailureBody(initialUrl, error),
5862
+ StandardCharsets.UTF_8
5863
+ );
5864
+ _webView.post(() -> {
5865
+ if (_webView == null) {
5866
+ return;
5867
+ }
5868
+ _webView.loadDataWithBaseURL(initialUrl, failureBody, "text/plain", "utf-8", initialUrl);
5869
+ });
5830
5870
  }
5831
5871
  });
5832
5872
  }
@@ -5973,6 +6013,13 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5973
6013
  return response;
5974
6014
  }
5975
6015
 
6016
+ private WebResourceResponse createProxiedNativeFailureResponse(String requestUrl, boolean bridgeBackedRequest, IOException error) {
6017
+ if (!ProxyRequestSupport.shouldReturnSyntheticNativeFailure(bridgeBackedRequest, _options, requestUrl)) {
6018
+ return null;
6019
+ }
6020
+ return createNativeFailureResponse(requestUrl, error);
6021
+ }
6022
+
5976
6023
  private WebResourceResponse createNativeFailureResponse(String requestUrl, IOException error) {
5977
6024
  NativeResponseData responseData = new NativeResponseData(
5978
6025
  ProxyRequestSupport.SYNTHETIC_NATIVE_FAILURE_STATUS,
@@ -6135,7 +6182,13 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6135
6182
  contentType = conn.getContentType();
6136
6183
  }
6137
6184
 
6138
- return new NativeResponseData(status, contentType, responseHeaders, bodyBytes);
6185
+ ProxyRequestSupport.ProxiedResponseMetadata normalizedResponse = ProxyRequestSupport.normalizeProxiedResponseMetadata(
6186
+ contentType,
6187
+ responseHeaders,
6188
+ requestContext.url
6189
+ );
6190
+
6191
+ return new NativeResponseData(status, normalizedResponse.contentType(), normalizedResponse.headers(), bodyBytes);
6139
6192
  } finally {
6140
6193
  conn.disconnect();
6141
6194
  }
@@ -6155,15 +6208,22 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6155
6208
  wroteCookie = true;
6156
6209
  }
6157
6210
  if (wroteCookie) {
6158
- cookieManager.flush();
6211
+ scheduleCookieFlush();
6159
6212
  }
6160
6213
  }
6161
6214
 
6162
- public void handleProxyResponse(String requestId, JSObject response) {
6163
- ProxiedRequest proxiedRequest;
6164
- synchronized (proxiedRequestsHashmap) {
6165
- proxiedRequest = proxiedRequestsHashmap.get(requestId);
6215
+ private void scheduleCookieFlush() {
6216
+ if (!cookieFlushScheduled.compareAndSet(false, true)) {
6217
+ return;
6166
6218
  }
6219
+ mainHandler.post(() -> {
6220
+ cookieFlushScheduled.set(false);
6221
+ CookieManager.getInstance().flush();
6222
+ });
6223
+ }
6224
+
6225
+ public void handleProxyResponse(String requestId, JSObject response) {
6226
+ ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(requestId);
6167
6227
  if (proxiedRequest == null) {
6168
6228
  Log.e("InAppBrowserProxy", "No pending request for id: " + requestId);
6169
6229
  return;
@@ -6175,9 +6235,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6175
6235
  }
6176
6236
 
6177
6237
  if (response == null) {
6178
- synchronized (proxiedRequestsHashmap) {
6179
- proxiedRequestsHashmap.remove(requestId);
6180
- }
6238
+ proxiedRequestsHashmap.remove(requestId);
6181
6239
  proxiedRequest.semaphore.release();
6182
6240
  return;
6183
6241
  }
@@ -6261,14 +6319,21 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6261
6319
  if (contentType == null) {
6262
6320
  contentType = responseHeaders.get("Content-Type");
6263
6321
  }
6264
- if (contentType == null) {
6265
- contentType = "application/octet-stream";
6266
- }
6322
+ ProxyRequestSupport.ProxiedResponseMetadata normalizedResponse = ProxyRequestSupport.normalizeProxiedResponseMetadata(
6323
+ contentType,
6324
+ responseHeaders,
6325
+ proxiedRequest.requestContext != null ? proxiedRequest.requestContext.url : null
6326
+ );
6267
6327
 
6268
6328
  if (status < 100 || status > 599) {
6269
6329
  status = 200;
6270
6330
  }
6271
- proxiedRequest.nativeResponse = new NativeResponseData(status, contentType, responseHeaders, bodyBytes);
6331
+ proxiedRequest.nativeResponse = new NativeResponseData(
6332
+ status,
6333
+ normalizedResponse.contentType(),
6334
+ normalizedResponse.headers(),
6335
+ bodyBytes
6336
+ );
6272
6337
  proxiedRequest.response = buildWebResourceResponse(proxiedRequest.nativeResponse);
6273
6338
  }
6274
6339
  } catch (IOException invalidBodyError) {
@@ -6279,17 +6344,13 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6279
6344
  }
6280
6345
  }
6281
6346
 
6282
- synchronized (proxiedRequestsHashmap) {
6283
- proxiedRequestsHashmap.remove(requestId);
6284
- }
6347
+ proxiedRequestsHashmap.remove(requestId);
6285
6348
  proxiedRequest.semaphore.release();
6286
6349
  }
6287
6350
 
6288
6351
  @Override
6289
6352
  public boolean hasPendingProxyRequest(String requestId) {
6290
- synchronized (proxiedRequestsHashmap) {
6291
- return proxiedRequestsHashmap.containsKey(requestId);
6292
- }
6353
+ return proxiedRequestsHashmap.containsKey(requestId);
6293
6354
  }
6294
6355
 
6295
6356
  private void shareUrl() {
@@ -98,7 +98,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
98
98
  case aware = "AWARE"
99
99
  case fakeVisible = "FAKE_VISIBLE"
100
100
  }
101
- private let pluginVersion: String = "8.7.5"
101
+ private let pluginVersion: String = "8.7.7"
102
102
  public let identifier = "CapgoInAppBrowserPlugin"
103
103
  public let jsName = "CapgoInAppBrowser"
104
104
  public let pluginMethods: [CAPPluginMethod] = [
@@ -2129,7 +2129,9 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
2129
2129
  responseDict = dict
2130
2130
  }
2131
2131
 
2132
- proxyHandler.handleResponse(requestId: requestId, phase: phase, responseData: responseDict)
2132
+ DispatchQueue.global(qos: .userInitiated).async { [proxyHandler] in
2133
+ proxyHandler.handleResponse(requestId: requestId, phase: phase, responseData: responseDict)
2134
+ }
2133
2135
  call.resolve()
2134
2136
  }
2135
2137
 
@@ -51,11 +51,8 @@ public final class ProxyBridge {
51
51
  func getAndRemove(requestId: String) -> ProxyBridgeStoredRequest? {
52
52
  lock.lock()
53
53
  defer { lock.unlock() }
54
- guard let storedRequest = storedRequests.removeValue(forKey: requestId) else {
55
- return nil
56
- }
57
- storedRequestOrder.removeAll { $0 == requestId }
58
- return storedRequest
54
+ // Avoid O(n) queue scans on every parallel bridge request; stale ids are purged lazily.
55
+ return storedRequests.removeValue(forKey: requestId)
59
56
  }
60
57
 
61
58
  private func cleanupExpiredRequests(now: Date) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-inappbrowser",
3
- "version": "8.7.5",
3
+ "version": "8.7.7",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",