Skip to content

Commit

Permalink
[webview_flutter_wkwebview] Fixes JSON.stringify() cannot serialize c…
Browse files Browse the repository at this point in the history
…yclic 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 flutter/flutter#144535
  • Loading branch information
LinXunFeng committed May 15, 2024
1 parent 2f35b83 commit 0870dc8
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 38 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -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>
1 change: 1 addition & 0 deletions packages/webview_flutter/webview_flutter_wkwebview/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}}'),
);
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,33 +630,65 @@ 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 log = {
level: type,
message: message
};
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
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
};
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
};
let originalLog = console.log;
let originalInfo = console.info;
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1378,31 +1378,49 @@ 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 log = {
level: type,
message: message
};
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
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
};
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
}
};
let originalLog = console.log;
let originalInfo = console.info;
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);
Expand Down

0 comments on commit 0870dc8

Please sign in to comment.