From 808b1d226dc01de0145253f3a094a87e11e96c04 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Wed, 28 Jan 2026 21:48:39 -0600 Subject: [PATCH 1/7] [webview_flutter_wkwebview] Fix crash when calling setOnConsoleMessage multiple times --- .../UserContentControllerProxyAPITests.swift | 50 +++++++++++++++++++ ...serContentControllerProxyAPIDelegate.swift | 3 ++ 2 files changed, 53 insertions(+) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift index b401c88ceacf..ab10b1eccdae 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift @@ -95,3 +95,53 @@ class TestUserContentController: WKUserContentController { removeAllUserScriptsCalled = true } } + +// Mock that simulates WKUserContentController's behavior of crashing/complaining +// when adding a duplicate script message handler. +// Mock that verifies remove is called before add, and enforces uniqueness +class MockVerifyingUserContentController: WKUserContentController { + var registeredNames: Set = [] + var removeCalledFor: String? = nil + + override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { + if registeredNames.contains(name) { + // If we are here, it means remove wasn't called or failed to remove + // In logical flow, remove() should have been called first. + // But implementation-wise, we just want to ensure we don't crash. + // Ideally, the 'remove' call below should have cleared it. + NSException(name: NSExceptionName.invalidArgumentException, reason: "Duplicate handler name", userInfo: nil).raise() + } + registeredNames.insert(name) + } + + override func removeScriptMessageHandler(forName name: String) { + removeCalledFor = name + registeredNames.remove(name) + } +} + +extension UserContentControllerProxyAPITests { + @MainActor func testAddScriptMessageHandlerHandlesDuplicates() { + let registrar = TestProxyApiRegistrar() + let api = registrar.apiDelegate.pigeonApiWKUserContentController(registrar) + + let instance = MockVerifyingUserContentController() + let handler = ScriptMessageHandlerImpl( + api: registrar.apiDelegate.pigeonApiWKScriptMessageHandler(registrar), registrar: registrar) + let name = "myString" + + // First add + try? api.pigeonDelegate.addScriptMessageHandler( + pigeonApi: api, pigeonInstance: instance, handler: handler, name: name) + XCTAssertTrue(instance.registeredNames.contains(name)) + + // Second add - should NOT crash because implementation calls remove first + try? api.pigeonDelegate.addScriptMessageHandler( + pigeonApi: api, pigeonInstance: instance, handler: handler, name: name) + + // Check that it's still registered (or re-registered) + XCTAssertTrue(instance.registeredNames.contains(name)) + // Verify remove was called + XCTAssertEqual(instance.removeCalledFor, name) + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift index d8cca6f2cda0..cfd88315e4ff 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift @@ -13,6 +13,9 @@ class UserContentControllerProxyAPIDelegate: PigeonApiDelegateWKUserContentContr pigeonApi: PigeonApiWKUserContentController, pigeonInstance: WKUserContentController, handler: WKScriptMessageHandler, name: String ) throws { + // WKUserContentController will crash if a script message handler with the same name + // is added twice. We remove the existing one (if any) to ensure the new one replaces it. + pigeonInstance.removeScriptMessageHandler(forName: name) pigeonInstance.add(handler, name: name) } From 68a33fa7dbfab9d9ce8d7daab47f76d819d85880 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Thu, 29 Jan 2026 11:26:22 -0600 Subject: [PATCH 2/7] Move console message logic to Dart --- .../UserContentControllerProxyAPITests.swift | 50 ------------------- ...serContentControllerProxyAPIDelegate.swift | 3 -- .../ios/Runner.xcodeproj/project.pbxproj | 6 +-- .../lib/src/webkit_webview_controller.dart | 8 ++- .../test/webkit_webview_controller_test.dart | 17 +++++++ 5 files changed, 25 insertions(+), 59 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift index ab10b1eccdae..b401c88ceacf 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UserContentControllerProxyAPITests.swift @@ -95,53 +95,3 @@ class TestUserContentController: WKUserContentController { removeAllUserScriptsCalled = true } } - -// Mock that simulates WKUserContentController's behavior of crashing/complaining -// when adding a duplicate script message handler. -// Mock that verifies remove is called before add, and enforces uniqueness -class MockVerifyingUserContentController: WKUserContentController { - var registeredNames: Set = [] - var removeCalledFor: String? = nil - - override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { - if registeredNames.contains(name) { - // If we are here, it means remove wasn't called or failed to remove - // In logical flow, remove() should have been called first. - // But implementation-wise, we just want to ensure we don't crash. - // Ideally, the 'remove' call below should have cleared it. - NSException(name: NSExceptionName.invalidArgumentException, reason: "Duplicate handler name", userInfo: nil).raise() - } - registeredNames.insert(name) - } - - override func removeScriptMessageHandler(forName name: String) { - removeCalledFor = name - registeredNames.remove(name) - } -} - -extension UserContentControllerProxyAPITests { - @MainActor func testAddScriptMessageHandlerHandlesDuplicates() { - let registrar = TestProxyApiRegistrar() - let api = registrar.apiDelegate.pigeonApiWKUserContentController(registrar) - - let instance = MockVerifyingUserContentController() - let handler = ScriptMessageHandlerImpl( - api: registrar.apiDelegate.pigeonApiWKScriptMessageHandler(registrar), registrar: registrar) - let name = "myString" - - // First add - try? api.pigeonDelegate.addScriptMessageHandler( - pigeonApi: api, pigeonInstance: instance, handler: handler, name: name) - XCTAssertTrue(instance.registeredNames.contains(name)) - - // Second add - should NOT crash because implementation calls remove first - try? api.pigeonDelegate.addScriptMessageHandler( - pigeonApi: api, pigeonInstance: instance, handler: handler, name: name) - - // Check that it's still registered (or re-registered) - XCTAssertTrue(instance.registeredNames.contains(name)) - // Verify remove was called - XCTAssertEqual(instance.removeCalledFor, name) - } -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift index cfd88315e4ff..d8cca6f2cda0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/UserContentControllerProxyAPIDelegate.swift @@ -13,9 +13,6 @@ class UserContentControllerProxyAPIDelegate: PigeonApiDelegateWKUserContentContr pigeonApi: PigeonApiWKUserContentController, pigeonInstance: WKUserContentController, handler: WKScriptMessageHandler, name: String ) throws { - // WKUserContentController will crash if a script message handler with the same name - // is added twice. We remove the existing one (if any) to ensure the new one replaces it. - pigeonInstance.removeScriptMessageHandler(forName: name) pigeonInstance.add(handler, name: name) } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 4755950f7c8f..008a476e36d8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -415,7 +415,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -502,12 +502,10 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/webview_flutter_wkwebview/webview_flutter_wkwebview_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/webview_flutter_wkwebview_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; @@ -944,7 +942,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 6eda612ef31b..f0319f21dffd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -700,7 +700,7 @@ class WebKitWebViewController extends PlatformWebViewController { @override Future setOnConsoleMessage( void Function(JavaScriptConsoleMessage consoleMessage) onConsoleMessage, - ) { + ) async { _onConsoleMessageCallback = onConsoleMessage; final JavaScriptChannelParams channelParams = WebKitJavaScriptChannelParams( @@ -736,7 +736,11 @@ class WebKitWebViewController extends PlatformWebViewController { }, ); - addJavaScriptChannel(channelParams); + if (_javaScriptChannelParams.containsKey('fltConsoleMessage')) { + await removeJavaScriptChannel('fltConsoleMessage'); + } + + await addJavaScriptChannel(channelParams); return _injectConsoleOverride(); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 579e84a22332..e6052e146f25 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -1815,6 +1815,23 @@ window.addEventListener("error", function(e) { expect(logs[JavaScriptLogLevel.log], 'Log message'); expect(logs[JavaScriptLogLevel.warning], 'Warning message'); }); + + test('setOnConsoleMessage called twice does not throw', () async { + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller.setOnConsoleMessage( + (JavaScriptConsoleMessage message) {}, + ); + await controller.setOnConsoleMessage( + (JavaScriptConsoleMessage message) {}, + ); + + verify(mockUserContentController.removeScriptMessageHandler('fltConsoleMessage')); + }); }); test('setOnCanGoBackChange', () async { From 729c432f7064fbf441528f8772a08476974f30a4 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Thu, 29 Jan 2026 11:33:35 -0600 Subject: [PATCH 3/7] Fix lint errors in test --- .../test/webkit_webview_controller_test.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index e6052e146f25..a399eed96ef3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -1817,8 +1817,7 @@ window.addEventListener("error", function(e) { }); test('setOnConsoleMessage called twice does not throw', () async { - final MockWKUserContentController mockUserContentController = - MockWKUserContentController(); + final mockUserContentController = MockWKUserContentController(); final WebKitWebViewController controller = createControllerWithMocks( mockUserContentController: mockUserContentController, ); @@ -1830,7 +1829,11 @@ window.addEventListener("error", function(e) { (JavaScriptConsoleMessage message) {}, ); - verify(mockUserContentController.removeScriptMessageHandler('fltConsoleMessage')); + verify( + mockUserContentController.removeScriptMessageHandler( + 'fltConsoleMessage', + ), + ); }); }); From a26fd8f1c620bd1172eecef4901e36818154848b Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Thu, 29 Jan 2026 11:35:56 -0600 Subject: [PATCH 4/7] Revert unintended project.pbxproj changes --- .../example/ios/Runner.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 008a476e36d8..4755950f7c8f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -415,7 +415,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -502,10 +502,12 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/webview_flutter_wkwebview/webview_flutter_wkwebview_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/webview_flutter_wkwebview_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; @@ -942,7 +944,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; From 4409d4a799dd4fe463f7ebc89a1dbacafdbbd0fe Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Thu, 29 Jan 2026 16:52:43 -0600 Subject: [PATCH 5/7] Bump version and update changelog --- .../webview_flutter/webview_flutter_wkwebview/CHANGELOG.md | 4 ++++ .../webview_flutter/webview_flutter_wkwebview/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 403c760d267c..04fbf5a81ac9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.23.7 + +* Fixes crash when calling setOnConsoleMessage multiple times. + ## 3.23.6 * Fixes a crash if WebViewFlutterWKWebViewExternalAPI is passed the wrong registry. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 9678b60179c2..5439ee7a5fb1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.23.6 +version: 3.23.7 environment: sdk: ^3.9.0 From 3931a749e3c9a122bfae2b22f481655a38b0a64b Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Wed, 4 Feb 2026 13:09:07 -0600 Subject: [PATCH 6/7] docs: Add comment about duplicate channel crash --- .../lib/src/webkit_webview_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index f0319f21dffd..6c356be98650 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -736,6 +736,7 @@ class WebKitWebViewController extends PlatformWebViewController { }, ); + // If fltConsoleMessage is already present, it would crash when adding it again. if (_javaScriptChannelParams.containsKey('fltConsoleMessage')) { await removeJavaScriptChannel('fltConsoleMessage'); } From 3c17cd2349ef1eb59bd45c1c7e562d1dd89cadb3 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Thu, 5 Feb 2026 09:38:09 -0600 Subject: [PATCH 7/7] Apply review optimization: verify channel presence --- .../lib/src/webkit_webview_controller.dart | 4 ++-- .../test/webkit_webview_controller_test.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 6c356be98650..8c7ef34cd325 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -736,9 +736,9 @@ class WebKitWebViewController extends PlatformWebViewController { }, ); - // If fltConsoleMessage is already present, it would crash when adding it again. + // If fltConsoleMessage is already present, the callback is already registered. if (_javaScriptChannelParams.containsKey('fltConsoleMessage')) { - await removeJavaScriptChannel('fltConsoleMessage'); + return; } await addJavaScriptChannel(channelParams); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index a399eed96ef3..3ec193176958 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -1829,7 +1829,7 @@ window.addEventListener("error", function(e) { (JavaScriptConsoleMessage message) {}, ); - verify( + verifyNever( mockUserContentController.removeScriptMessageHandler( 'fltConsoleMessage', ),