From 0870dc84d364aad79fba7a57ef2984b01b073c4d Mon Sep 17 00:00:00 2001 From: LinXunFeng Date: Wed, 15 May 2024 09:19:58 +0800 Subject: [PATCH] [webview_flutter_wkwebview] Fixes JSON.stringify() cannot serialize cyclic structures (#6274) Using the `replacer` parameter of `JSON.stringify()` to remove cyclic object to resolve the following error. ``` TypeError: JSON.stringify cannot serialize cyclic structures. ``` ~~Related solution: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value~~ Fixes https://github.com/flutter/flutter/issues/144535 --- AUTHORS | 3 +- .../webview_flutter_wkwebview/AUTHORS | 1 + .../webview_flutter_wkwebview/CHANGELOG.md | 4 ++ .../webview_flutter_test.dart | 58 +++++++++++++++++ .../lib/src/webkit_webview_controller.dart | 64 ++++++++++++++----- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- .../test/webkit_webview_controller_test.dart | 50 ++++++++++----- 7 files changed, 148 insertions(+), 34 deletions(-) diff --git a/AUTHORS b/AUTHORS index 543b9813fe..2d6cec4ff9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -74,4 +74,5 @@ Twin Sun, LLC Amir Panahandeh Daniele Cambi Michele Benedetti -Taskulu LDA \ No newline at end of file +Taskulu LDA +LinXunFeng diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS index a0cf8d9f1c..8625e8aaa7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS +++ b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS @@ -68,3 +68,4 @@ Maurits van Beusekom Antonino Di Natale Nick Bradshaw The Vinh Luong +LinXunFeng diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 70ba80cee6..14f55957ad 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.13.1 + +* Fixes `JSON.stringify()` cannot serialize cyclic structures. + ## 3.13.0 * Adds `decidePolicyForNavigationResponse` to internal WKNavigationDelegate to support the diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 3600d064d0..4a179696b7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -1525,6 +1525,64 @@ Future main() async { await expectLater( debugMessageReceived.future, completion('debug:Debug message')); }); + + testWidgets('can receive console log messages with cyclic object value', + (WidgetTester tester) async { + const String testPage = ''' + + + + WebResourceError test + + + + + '''; + + final Completer debugMessageReceived = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + + await controller + .setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) { + debugMessageReceived + .complete('${consoleMessage.level.name}:${consoleMessage.message}'); + }); + + await controller.loadHtmlString(testPage); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await expectLater( + debugMessageReceived.future, + completion( + 'log:{"obj1":{"name":"obj1"},"obj2":{"name":"obj2","obj1":{"name":"obj1"}}}'), + ); + }); }); } 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 b9e9386239..c152f32625 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 @@ -630,21 +630,53 @@ class WebKitWebViewController extends PlatformWebViewController { } Future _injectConsoleOverride() { + // Within overrideScript, a series of console output methods such as + // console.log will be rewritten to pass the output content to the Flutter + // end. + // + // These output contents will first be serialized through JSON.stringify(), + // but if the output content contains cyclic objects, it will encounter the + // following error. + // TypeError: JSON.stringify cannot serialize cyclic structures. + // See https://github.com/flutter/flutter/issues/144535. + // + // Considering this is just looking at the logs printed via console.log, + // the cyclic object is not important, so remove it. + // Therefore, the replacer parameter of JSON.stringify() is used and the + // removeCyclicObject method is passed in to solve the error. const WKUserScript overrideScript = WKUserScript( ''' -function log(type, args) { - var message = Object.values(args) - .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString()) - .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars - .join(", "); +var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || { + removeCyclicObject: function() { + const traversalStack = []; + return function (k, v) { + if (typeof v !== "object" || v === null) { return v; } + const currentParentObj = this; + while ( + traversalStack.length > 0 && + traversalStack[traversalStack.length - 1] !== currentParentObj + ) { + traversalStack.pop(); + } + if (traversalStack.includes(v)) { return; } + traversalStack.push(v); + return v; + }; + }, + log: function (type, args) { + var message = Object.values(args) + .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString()) + .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars + .join(", "); - var log = { - level: type, - message: message - }; + var log = { + level: type, + message: message + }; - window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); -} + window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); + } +}; let originalLog = console.log; let originalInfo = console.info; @@ -652,11 +684,11 @@ let originalWarn = console.warn; let originalError = console.error; let originalDebug = console.debug; -console.log = function() { log("log", arguments); originalLog.apply(null, arguments) }; -console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) }; -console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) }; -console.error = function() { log("error", arguments); originalError.apply(null, arguments) }; -console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) }; +console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) }; +console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) }; +console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) }; +console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) }; +console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) }; window.addEventListener("error", function(e) { log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index bffa7eaf66..51d28e2191 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.13.0 +version: 3.13.1 environment: sdk: ^3.2.3 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 a28eca241a..b5f731f973 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 @@ -1378,19 +1378,37 @@ void main() { expect(overrideConsoleScript.injectionTime, WKUserScriptInjectionTime.atDocumentStart); expect(overrideConsoleScript.source, ''' -function log(type, args) { - var message = Object.values(args) - .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString()) - .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars - .join(", "); +var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || { + removeCyclicObject: function() { + const traversalStack = []; + return function (k, v) { + if (typeof v !== "object" || v === null) { return v; } + const currentParentObj = this; + while ( + traversalStack.length > 0 && + traversalStack[traversalStack.length - 1] !== currentParentObj + ) { + traversalStack.pop(); + } + if (traversalStack.includes(v)) { return; } + traversalStack.push(v); + return v; + }; + }, + log: function (type, args) { + var message = Object.values(args) + .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString()) + .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars + .join(", "); - var log = { - level: type, - message: message - }; + var log = { + level: type, + message: message + }; - window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); -} + window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); + } +}; let originalLog = console.log; let originalInfo = console.info; @@ -1398,11 +1416,11 @@ let originalWarn = console.warn; let originalError = console.error; let originalDebug = console.debug; -console.log = function() { log("log", arguments); originalLog.apply(null, arguments) }; -console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) }; -console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) }; -console.error = function() { log("error", arguments); originalError.apply(null, arguments) }; -console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) }; +console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) }; +console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) }; +console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) }; +console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) }; +console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) }; window.addEventListener("error", function(e) { log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);