[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
This commit is contained in:
LinXunFeng
2024-05-15 09:19:58 +08:00
committed by GitHub
parent 2f35b836a2
commit 0870dc84d3
7 changed files with 148 additions and 34 deletions

View File

@ -74,4 +74,5 @@ Twin Sun, LLC <google-contrib@twinsunsolutions.com>
Amir Panahandeh <amirpanahandeh@gmail.com>
Daniele Cambi <dancam.dev@gmail.com>
Michele Benedetti <michelebenx98@gmail.com>
Taskulu LDA <contributions@taskulu.com>
Taskulu LDA <contributions@taskulu.com>
LinXunFeng <linxunfeng@yeah.net>

View File

@ -68,3 +68,4 @@ Maurits van Beusekom <maurits@baseflow.com>
Antonino Di Natale <gyorgio88@gmail.com>
Nick Bradshaw <nickalasb@gmail.com>
The Vinh Luong <ltv.luongthevinh@gmail.com>
LinXunFeng <linxunfeng@yeah.net>

View File

@ -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

View File

@ -1525,6 +1525,64 @@ Future<void> 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 = '''
<!DOCTYPE html>
<html>
<head>
<title>WebResourceError test</title>
<script type="text/javascript">
function onLoad() {
const obj1 = {
name: "obj1",
};
const obj2 = {
name: "obj2",
obj1: obj1,
};
const obj = {
obj1: obj1,
obj2: obj2,
};
obj.self = obj;
console.log(obj);
}
</script>
</head>
<body onload="onLoad();">
</html>
''';
final Completer<String> debugMessageReceived = Completer<String>();
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"}}}'),
);
});
});
}

View File

@ -630,21 +630,53 @@ class WebKitWebViewController extends PlatformWebViewController {
}
Future<void> _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);

View File

@ -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

View File

@ -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);