-
Notifications
You must be signed in to change notification settings - Fork 3
feat: support async onShouldStartLoad requests #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Thibault Malbranche <malbranche.thibault@gmail.com>
| int lockIdentifier = [[RNCWebViewDecisionManager getInstance] setDecisionHandler:^(BOOL shouldStart) { | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| if (!shouldStart) { | ||
| decisionHandler(WKNavigationActionPolicyCancel); | ||
| return; | ||
| } | ||
| allowNavigation(); | ||
| }); | ||
| }]; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allowing code is moved into the allowNavigation helper to be reused below to keep the old fallback behavior
apple/RNCWebViewManager.m
Outdated
| RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " | ||
| "got %lld, expected %lld", (long long)lockIdentifier, (long long)_shouldStartLoadLock.condition); | ||
| } | ||
| [[RNCWebViewDecisionManager getInstance] setResult:result forLockIdentifier:(int)lockIdentifier]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kbulgakov-exo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tACK
- WebView loads content normally when JS responds with
true - WebView rejects loading when JS responds with
false
guten-exodus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK nice
|
@guten-exodus Could you check whether this also addresses the pain points you had in Pay? |
timlanahan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ChALkeR please review
Tested, worked great! |
kewdex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timlanahan This should be tested in an emulator by someone actively trying to bypass that, including (but not limited to) in the following conditions:
- main thread hanged
- main thread is slow and does not respond in time
- there are several requests queued some of which should pass and some should not - we should not load the non-passing one
- (3) but main thread is slow
- handler was not even installed in time before the load happened
- All of the above but the load requests are spammed, mixing allowed and non-allowed ones
The current test plan is unsatisfactory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR migrates from a synchronous blocking approach to an asynchronous architecture for handling onShouldStartLoad navigation decisions in iOS/macOS WebView. The synchronous approach was causing reliability issues when the JavaScript thread couldn't respond within the 500ms timeout, leading to blocked navigations even when JS eventually responded with "allow".
Changes:
- Introduced
RNCWebViewDecisionManagersingleton to manage async navigation decisions across threads - Removed synchronous
RNCWebViewDelegateprotocol and blocking timeout logic - Updated navigation decision flow to store handlers and resolve them asynchronously when JS responds
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apple/RNCWebViewDecisionManager.h | New header defining the decision manager interface with thread-safe handler storage |
| apple/RNCWebViewDecisionManager.m | Implementation of singleton pattern for managing async navigation decision handlers |
| apple/RNCWebViewManager.m | Simplified to delegate decision resolution to RNCWebViewDecisionManager, removed blocking logic |
| apple/RNCWebView.h | Removed RNCWebViewDelegate protocol definition |
| apple/RNCWebView.m | Updated decidePolicyForNavigationAction to use async decision handlers instead of synchronous delegate calls |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| */ | ||
| - (int)setDecisionHandler:(DecisionBlock)decisionHandler { | ||
| @synchronized (self) { | ||
| int lockIdentifier = self.nextLockIdentifier++; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nextLockIdentifier counter could theoretically overflow after 2^31-1 increments (about 2.1 billion). While unlikely in normal usage, if this happens, the identifier will wrap around to negative values or repeat, potentially causing collisions with pending navigation decisions. Consider using a more robust identifier generation strategy, such as checking for existing keys before assignment, or using a larger integer type.
| @interface RNCWebViewDecisionManager : NSObject { | ||
| int nextLockIdentifier; | ||
| NSMutableDictionary *decisionHandlers; | ||
| } | ||
|
|
||
| @property (nonatomic) int nextLockIdentifier; | ||
| @property (nonatomic, retain) NSMutableDictionary *decisionHandlers; | ||
|
|
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instance variables in the @interface block are redundant when @Property declarations are present, as properties automatically synthesize their backing instance variables. The duplicate declarations of nextLockIdentifier and decisionHandlers in lines 6-7 should be removed to follow modern Objective-C conventions.
| @interface RNCWebViewDecisionManager : NSObject { | |
| int nextLockIdentifier; | |
| NSMutableDictionary *decisionHandlers; | |
| } | |
| @property (nonatomic) int nextLockIdentifier; | |
| @property (nonatomic, retain) NSMutableDictionary *decisionHandlers; | |
| @interface RNCWebViewDecisionManager : NSObject | |
| @property (nonatomic) int nextLockIdentifier; | |
| @property (nonatomic, retain) NSMutableDictionary *decisionHandlers; |
| } | ||
|
|
||
| @property (nonatomic) int nextLockIdentifier; | ||
| @property (nonatomic, retain) NSMutableDictionary *decisionHandlers; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decisionHandlers property should use 'copy' semantic instead of 'retain' for storing blocks. In Objective-C, blocks need to be copied to move them from the stack to the heap to ensure they remain valid after the scope in which they were created ends. This is a critical memory management issue that could lead to crashes when the block is invoked after the original stack frame is destroyed.
| int lockIdentifier = [[RNCWebViewDecisionManager getInstance] setDecisionHandler:^(BOOL shouldStart) { | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| if (!shouldStart) { | ||
| decisionHandler(WKNavigationActionPolicyCancel); | ||
| return; | ||
| } | ||
| allowNavigation(); | ||
| }); | ||
| }]; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the WKWebView is deallocated while there are pending navigation decisions, the stored decision handlers in the singleton RNCWebViewDecisionManager will never be called, leading to a memory leak. The decisionHandler closure from WKWebView's decidePolicyForNavigationAction captures references that won't be released until the handler is invoked. Consider adding a mechanism to cancel pending decisions when the webview is deallocated, or add a timeout mechanism to automatically clean up stale handlers.
| - (void) setResult:(BOOL)shouldStart | ||
| forLockIdentifier:(int)lockIdentifier { | ||
| DecisionBlock handler; | ||
| @synchronized (self) { | ||
| handler = [self.decisionHandlers objectForKey:@(lockIdentifier)]; | ||
| if (handler == nil) { | ||
| RCTLogWarn(@"Lock not found"); | ||
| return; | ||
| } | ||
| [self.decisionHandlers removeObjectForKey:@(lockIdentifier)]; | ||
| } | ||
| handler(shouldStart); |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WKWebView decisionHandler must be called exactly once. If JS calls startLoadWithResult multiple times with the same lockIdentifier (either due to a bug or race condition), the decisionHandler will be invoked multiple times, which will cause a crash with the error "Completion handler passed to -[WKWebView decidePolicyForNavigationAction:decisionHandler:] was called more than once". Consider adding a guard to track whether a decision has already been made for a given lockIdentifier to prevent double invocation.
|
There’s an ongoing effort to upgrade |
The existing approach with locking the main thread to make a synchronous call asking JS "can I proceed with this URL" isn't reliable. Even the recent timeout bump to 500ms #47 didn't help, we still experience the loading issues. Bumping the timeout even more (for example with #48) would inevitably lead to performance degradation because of the locked thread. We should come up with a more reliable solution
The bottleneck
I've put some logs to diagnose what exactly is the bottleneck. The JS callback works super fast, but the problem is that sometimes it seems to be called after Native already makes the decision after the 500ms default timeout:
The solution
Let's mirror the async architecture of the
onShouldStartLoadcalls from https://github.com/react-native-webview/react-native-webview. We can easily backport most of the code.I recommend reviewing commit by commit
Notes:
WebViewDecisionManagerfrom upstream with minor formatting changesTesting
truefalse