Skip to content
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 @@ -1522,6 +1522,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,11 +630,34 @@ class WebKitWebViewController extends PlatformWebViewController {
}

Future<void> _injectConsoleOverride() {
// Using the replacer parameter of JSON.stringify() to solve the 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.
const WKUserScript overrideScript = WKUserScript(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment briefly explaining the approach at a high level so readers don't need to reverse-engineer what is being done here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'''
function removeCyclicObject() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a name that cannot plausibly cause collisions on actual web pages, since it is injected into the page's context. E.g., maybe a Flutter prefix and a dynamically generated UUID? @ditman Any suggestions on best practice for avoiding naming collisions in JS?

@ditman ditman Apr 25, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stuartmorgan At the expense of some performance this function could be defined inside of the log function that calls it (line 659), and that way it wouldn't be directly exposed to the host page... But if we're not worried about a function log colliding with the functions of a webpage, I wouldn't worry too much about removeCyclicObject either :)

(In the engine we normally prefix with _ the name of the function so there's a fewer change of a global collision, but there's no guarantee that it won't collide. For a UUID you'd have to strip the - characters for it to be a valid function name, but it could definitely work)

((PS: Generating a UUID in JS: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID, has just become "available" to Flutter web according to our Supported platforms))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the expense of some performance this function could be defined inside of the log function that calls it (line 659), and that way it wouldn't be directly exposed to the host page

That sounds worth doing. If people are doing performance-affecting levels of print logging (and enabling this feature in release builds, which hopefully they aren't), they have problems already.

But if we're not worried about a function log colliding with the functions of a webpage, I wouldn't worry too much about removeCyclicObject either :)

😬 I am very concerned about that, I just missed it somehow. We should absolutely rename that.

Maybe UUID is overkill; we could also just do something like _flutter_log_override that's pretty unlikely to collide.

((PS: Generating a UUID in JS: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID, has just become "available" to Flutter web according to our Supported platforms))

We wouldn't need to do it in JS; we'd generate it in Dart, and then string interpolate that into the string we inject.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't need to do it in JS; we'd generate it in Dart, and then string interpolate that into the string we inject.

Ah you're right, it's "free" then. _flutter_log_override sounds like a good name. You can even have a small _flutter_webview_plugin_overrides "namespace" object that contains all the methods that you're injecting, so it's easy to hide/move them around.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const levelObjects = [];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this naming means; what is a "level object"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

levelObjects is used to record objects whose properties are currently being traversed. (Maybe I should rename it to stack or something? )

As shown in the figure below, the properties of the outermost object (blue box) are currently being traversed, and obj1 (green box) is also an object, so the properties of obj1 will be traversed next and pushed in levelObjects.

image

When traversing all the properties of obj1 (green box) and starting to traverse obj2, currentParentObj will point to the outermost object (blue box) and the last element obj1 (green box) of the current levelObjects is not equal, then obj1 will be popped up, and obj2 will be pushed to levelObjects.

image

return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
const currentParentObj = this;
while (
levelObjects.length > 0 &&
levelObjects[levelObjects.length - 1] !== currentParentObj
) {
levelObjects.pop();
}
if (levelObjects.includes(v)) { return; }
levelObjects.push(v);
return v;
};
}

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 => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

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,9 +1378,26 @@ void main() {
expect(overrideConsoleScript.injectionTime,
WKUserScriptInjectionTime.atDocumentStart);
expect(overrideConsoleScript.source, '''
function removeCyclicObject() {
const levelObjects = [];
return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
const currentParentObj = this;
while (
levelObjects.length > 0 &&
levelObjects[levelObjects.length - 1] !== currentParentObj
) {
levelObjects.pop();
}
if (levelObjects.includes(v)) { return; }
levelObjects.push(v);
return v;
};
}

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 => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

Expand Down