Skip to content

Commit 0870dc8

Browse files
authored
[webview_flutter_wkwebview] Fixes JSON.stringify() cannot serialize cyclic structures (flutter#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 flutter#144535
1 parent 2f35b83 commit 0870dc8

File tree

7 files changed

+152
-38
lines changed

7 files changed

+152
-38
lines changed

AUTHORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ Twin Sun, LLC <[email protected]>
7474
Amir Panahandeh <[email protected]>
7575
Daniele Cambi <[email protected]>
7676
Michele Benedetti <[email protected]>
77-
Taskulu LDA <[email protected]>
77+
Taskulu LDA <[email protected]>
78+
LinXunFeng <[email protected]>

packages/webview_flutter/webview_flutter_wkwebview/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ Maurits van Beusekom <[email protected]>
6868
Antonino Di Natale <[email protected]>
6969
Nick Bradshaw <[email protected]>
7070
The Vinh Luong <[email protected]>
71+
LinXunFeng <[email protected]>

packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.13.1
2+
3+
* Fixes `JSON.stringify()` cannot serialize cyclic structures.
4+
15
## 3.13.0
26

37
* Adds `decidePolicyForNavigationResponse` to internal WKNavigationDelegate to support the

packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,64 @@ Future<void> main() async {
15251525
await expectLater(
15261526
debugMessageReceived.future, completion('debug:Debug message'));
15271527
});
1528+
1529+
testWidgets('can receive console log messages with cyclic object value',
1530+
(WidgetTester tester) async {
1531+
const String testPage = '''
1532+
<!DOCTYPE html>
1533+
<html>
1534+
<head>
1535+
<title>WebResourceError test</title>
1536+
<script type="text/javascript">
1537+
function onLoad() {
1538+
const obj1 = {
1539+
name: "obj1",
1540+
};
1541+
const obj2 = {
1542+
name: "obj2",
1543+
obj1: obj1,
1544+
};
1545+
const obj = {
1546+
obj1: obj1,
1547+
obj2: obj2,
1548+
};
1549+
obj.self = obj;
1550+
console.log(obj);
1551+
}
1552+
</script>
1553+
</head>
1554+
<body onload="onLoad();">
1555+
</html>
1556+
''';
1557+
1558+
final Completer<String> debugMessageReceived = Completer<String>();
1559+
final PlatformWebViewController controller = PlatformWebViewController(
1560+
const PlatformWebViewControllerCreationParams(),
1561+
);
1562+
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
1563+
1564+
await controller
1565+
.setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
1566+
debugMessageReceived
1567+
.complete('${consoleMessage.level.name}:${consoleMessage.message}');
1568+
});
1569+
1570+
await controller.loadHtmlString(testPage);
1571+
1572+
await tester.pumpWidget(Builder(
1573+
builder: (BuildContext context) {
1574+
return PlatformWebViewWidget(
1575+
PlatformWebViewWidgetCreationParams(controller: controller),
1576+
).build(context);
1577+
},
1578+
));
1579+
1580+
await expectLater(
1581+
debugMessageReceived.future,
1582+
completion(
1583+
'log:{"obj1":{"name":"obj1"},"obj2":{"name":"obj2","obj1":{"name":"obj1"}}}'),
1584+
);
1585+
});
15281586
});
15291587
}
15301588

packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -630,33 +630,65 @@ class WebKitWebViewController extends PlatformWebViewController {
630630
}
631631

632632
Future<void> _injectConsoleOverride() {
633+
// Within overrideScript, a series of console output methods such as
634+
// console.log will be rewritten to pass the output content to the Flutter
635+
// end.
636+
//
637+
// These output contents will first be serialized through JSON.stringify(),
638+
// but if the output content contains cyclic objects, it will encounter the
639+
// following error.
640+
// TypeError: JSON.stringify cannot serialize cyclic structures.
641+
// See https://github.com/flutter/flutter/issues/144535.
642+
//
643+
// Considering this is just looking at the logs printed via console.log,
644+
// the cyclic object is not important, so remove it.
645+
// Therefore, the replacer parameter of JSON.stringify() is used and the
646+
// removeCyclicObject method is passed in to solve the error.
633647
const WKUserScript overrideScript = WKUserScript(
634648
'''
635-
function log(type, args) {
636-
var message = Object.values(args)
637-
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
638-
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
639-
.join(", ");
640-
641-
var log = {
642-
level: type,
643-
message: message
644-
};
645-
646-
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
647-
}
649+
var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || {
650+
removeCyclicObject: function() {
651+
const traversalStack = [];
652+
return function (k, v) {
653+
if (typeof v !== "object" || v === null) { return v; }
654+
const currentParentObj = this;
655+
while (
656+
traversalStack.length > 0 &&
657+
traversalStack[traversalStack.length - 1] !== currentParentObj
658+
) {
659+
traversalStack.pop();
660+
}
661+
if (traversalStack.includes(v)) { return; }
662+
traversalStack.push(v);
663+
return v;
664+
};
665+
},
666+
log: function (type, args) {
667+
var message = Object.values(args)
668+
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString())
669+
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
670+
.join(", ");
671+
672+
var log = {
673+
level: type,
674+
message: message
675+
};
676+
677+
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
678+
}
679+
};
648680
649681
let originalLog = console.log;
650682
let originalInfo = console.info;
651683
let originalWarn = console.warn;
652684
let originalError = console.error;
653685
let originalDebug = console.debug;
654686
655-
console.log = function() { log("log", arguments); originalLog.apply(null, arguments) };
656-
console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) };
657-
console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) };
658-
console.error = function() { log("error", arguments); originalError.apply(null, arguments) };
659-
console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) };
687+
console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) };
688+
console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) };
689+
console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) };
690+
console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) };
691+
console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) };
660692
661693
window.addEventListener("error", function(e) {
662694
log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);

packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: webview_flutter_wkwebview
22
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
33
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
5-
version: 3.13.0
5+
version: 3.13.1
66

77
environment:
88
sdk: ^3.2.3

packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,31 +1378,49 @@ void main() {
13781378
expect(overrideConsoleScript.injectionTime,
13791379
WKUserScriptInjectionTime.atDocumentStart);
13801380
expect(overrideConsoleScript.source, '''
1381-
function log(type, args) {
1382-
var message = Object.values(args)
1383-
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
1384-
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
1385-
.join(", ");
1386-
1387-
var log = {
1388-
level: type,
1389-
message: message
1390-
};
1391-
1392-
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
1393-
}
1381+
var _flutter_webview_plugin_overrides = _flutter_webview_plugin_overrides || {
1382+
removeCyclicObject: function() {
1383+
const traversalStack = [];
1384+
return function (k, v) {
1385+
if (typeof v !== "object" || v === null) { return v; }
1386+
const currentParentObj = this;
1387+
while (
1388+
traversalStack.length > 0 &&
1389+
traversalStack[traversalStack.length - 1] !== currentParentObj
1390+
) {
1391+
traversalStack.pop();
1392+
}
1393+
if (traversalStack.includes(v)) { return; }
1394+
traversalStack.push(v);
1395+
return v;
1396+
};
1397+
},
1398+
log: function (type, args) {
1399+
var message = Object.values(args)
1400+
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, _flutter_webview_plugin_overrides.removeCyclicObject()) : v.toString())
1401+
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
1402+
.join(", ");
1403+
1404+
var log = {
1405+
level: type,
1406+
message: message
1407+
};
1408+
1409+
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
1410+
}
1411+
};
13941412
13951413
let originalLog = console.log;
13961414
let originalInfo = console.info;
13971415
let originalWarn = console.warn;
13981416
let originalError = console.error;
13991417
let originalDebug = console.debug;
14001418
1401-
console.log = function() { log("log", arguments); originalLog.apply(null, arguments) };
1402-
console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) };
1403-
console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) };
1404-
console.error = function() { log("error", arguments); originalError.apply(null, arguments) };
1405-
console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) };
1419+
console.log = function() { _flutter_webview_plugin_overrides.log("log", arguments); originalLog.apply(null, arguments) };
1420+
console.info = function() { _flutter_webview_plugin_overrides.log("info", arguments); originalInfo.apply(null, arguments) };
1421+
console.warn = function() { _flutter_webview_plugin_overrides.log("warning", arguments); originalWarn.apply(null, arguments) };
1422+
console.error = function() { _flutter_webview_plugin_overrides.log("error", arguments); originalError.apply(null, arguments) };
1423+
console.debug = function() { _flutter_webview_plugin_overrides.log("debug", arguments); originalDebug.apply(null, arguments) };
14061424
14071425
window.addEventListener("error", function(e) {
14081426
log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);

0 commit comments

Comments
 (0)