From 34d97ae30b8e76bd98b23bda3cba12954cd968ca Mon Sep 17 00:00:00 2001 From: Noah Godel Date: Thu, 5 Mar 2026 13:09:17 +0100 Subject: [PATCH 1/2] Fix WebKit completion handlers to also be called when delegate is nil Guard against nil delegate in ColdBootVisit's authentication challenge handler and WKUIController's alert/confirm panel handlers. When the delegate is deallocated before WebKit fires the callback, the completion handler was silently dropped, causing an NSInternalInconsistencyException crash on the main thread. --- Source/Turbo/Navigator/WKUIController.swift | 12 ++++++++++-- Source/Turbo/Visit/ColdBootVisit.swift | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Source/Turbo/Navigator/WKUIController.swift b/Source/Turbo/Navigator/WKUIController.swift index 0cfb4254..394d4c40 100644 --- a/Source/Turbo/Navigator/WKUIController.swift +++ b/Source/Turbo/Navigator/WKUIController.swift @@ -13,14 +13,22 @@ open class WKUIController: NSObject, WKUIDelegate { } open func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + guard let delegate else { + completionHandler() + return + } let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in completionHandler() }) - delegate?.present(alert, animated: true) + delegate.present(alert, animated: true) } open func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + guard let delegate else { + completionHandler(false) + return + } let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in completionHandler(true) @@ -28,6 +36,6 @@ open class WKUIController: NSObject, WKUIDelegate { alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completionHandler(false) }) - delegate?.present(alert, animated: true) + delegate.present(alert, animated: true) } } diff --git a/Source/Turbo/Visit/ColdBootVisit.swift b/Source/Turbo/Visit/ColdBootVisit.swift index e3e645f2..757eb6c9 100644 --- a/Source/Turbo/Visit/ColdBootVisit.swift +++ b/Source/Turbo/Visit/ColdBootVisit.swift @@ -128,7 +128,11 @@ extension ColdBootVisit: WKNavigationDelegate { } func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - delegate?.visit(self, didReceiveAuthenticationChallenge: challenge, completionHandler: completionHandler) + guard let delegate else { + completionHandler(.performDefaultHandling, nil) + return + } + delegate.visit(self, didReceiveAuthenticationChallenge: challenge, completionHandler: completionHandler) } } From ccdabcf6f7efa623081f2c516fe137183af5267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 2 Jul 2026 11:54:54 +0200 Subject: [PATCH 2/2] Guard auth challenge completion handler across the delegate chain The completion handler is forwarded through three weak-delegate hops. The leaf (ColdBootVisit) already falls back to .performDefaultHandling when its delegate is nil, but Session and Navigator still forwarded through an optional delegate, silently dropping the handler if that link was nil and crashing WebKit with NSInternalInconsistencyException. Guard both upstream hops with the same .performDefaultHandling fallback, consistent with the NavigatorDelegate default implementation. --- Source/Turbo/Navigator/Navigator.swift | 6 +++++- Source/Turbo/Session/Session.swift | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Source/Turbo/Navigator/Navigator.swift b/Source/Turbo/Navigator/Navigator.swift index 9c5b0655..05ce60fa 100644 --- a/Source/Turbo/Navigator/Navigator.swift +++ b/Source/Turbo/Navigator/Navigator.swift @@ -222,7 +222,11 @@ extension Navigator: SessionDelegate { } public func session(_ session: Session, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - delegate?.didReceiveAuthenticationChallenge(challenge, completionHandler: completionHandler) + guard let delegate else { + completionHandler(.performDefaultHandling, nil) + return + } + delegate.didReceiveAuthenticationChallenge(challenge, completionHandler: completionHandler) } public func sessionDidFinishRequest(_ session: Session) { diff --git a/Source/Turbo/Session/Session.swift b/Source/Turbo/Session/Session.swift index 7aebec5d..483d45b1 100644 --- a/Source/Turbo/Session/Session.swift +++ b/Source/Turbo/Session/Session.swift @@ -219,7 +219,11 @@ extension Session: VisitDelegate { } func visit(_ visit: Visit, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - delegate?.session(self, didReceiveAuthenticationChallenge: challenge, completionHandler: completionHandler) + guard let delegate else { + completionHandler(.performDefaultHandling, nil) + return + } + delegate.session(self, didReceiveAuthenticationChallenge: challenge, completionHandler: completionHandler) } func visitDidProposeVisitToLocation(_ location: URL) {