Skip to content

webview: unwrap null and non-JSON numbers in the Chrome console callback - #39064

Open
robobun wants to merge 1 commit into
mainfrom
farm/9d736bc0/webview-chrome-console-null-nan
Open

robobun wants to merge 1 commit into
mainfrom
farm/9d736bc0/webview-chrome-console-null-nan

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • new Bun.WebView({ backend: "chrome", console: (type, ...args) => ... }): a page-side console.log(null) hands the callback { type: "object", subtype: "null", value: null } instead of null.
  • Same callback: console.log(NaN, Infinity, -Infinity, -0) in the page arrives as 0, 0, 0, 0.
  • console: globalThis.console is affected the same way, since it shares the conversion: the parent prints the null wrapper as an object and the four numbers as 0.
  • The console option is documented (docs/runtime/webview.mdx, ConsoleCapture in packages/bun-types/bun.d.ts) as unwrapping primitive arguments to their raw values; only objects are supposed to arrive as the CDP descriptor.
  • Cause: remoteToJS in the Runtime.consoleAPICalled handler (src/runtime/webview/ChromeBackend.cpp, Transport::handleEvent) only looks at RemoteObject.type and RemoteObject.value. CDP encodes null as type: "object" with subtype: "null", so it fell into the object branch; and for numbers JSON cannot represent V8 omits value and sends unserializableValue instead, so getDouble("value").value_or(0) produced 0.

Fix

  • type: "number" without a value field now maps unserializableValue "-0" / "Infinity" / "-Infinity" to those numbers and anything else ("NaN") to NaN.
  • type: "object" with subtype: "null" now returns null before the generic object branch.
  • This is the behavior the option documents, and it is what the WebKit backend already does for null (its arguments travel through JSON.stringify, so null round-trips as null). Regular numbers, strings, booleans, undefined, the bigint/symbol description strings, and object descriptors are unchanged.
  • evaluate() decodes Runtime.evaluate results in a separate code path (Method::RuntimeEvaluate) and has a different documented contract (a JSON round trip), so it is intentionally not touched here; its handling of these numbers is tracked separately.
  • Tests, in test/js/bun/webview/webview-chrome.test.ts:
    • new chrome: console callback unwraps args whose RemoteObject has no value field asserts the whole callback argument list for null, NaN, Infinity, -Infinity, -0 (plus undefined, bigint and symbol, which share the "no value field" shape) and checks -0 with Object.is.
    • chrome: console: globalThis.console forwards to parent's stdout additionally logs the same values and now asserts the exact stdout (null NaN Infinity -Infinity -0), covering the ConsoleClient dispatch path.
    • Before the fix both fail (null arrives as the wrapper object, the numbers as 0); with the fix the whole file passes (52 tests). The container runs as root, so the runs used BUN_CHROME_PATH pointing at a wrapper that adds --no-sandbox; both the runtime and the test file's findChrome() honor that variable.
    • Also ran the console tests with BUN_JSC_validateExceptionChecks=1, clean.

Background

  • CDP (Chrome DevTools Protocol) is the JSON protocol the Chrome backend speaks to the browser. Runtime.consoleAPICalled is the event Chrome emits for each page-side console.* call; its args are RemoteObjects, not the values themselves.
  • A RemoteObject describes a value: type (string, number, boolean, undefined, bigint, symbol, object, function), an optional subtype for objects (null, array, date, ...), and for primitives either value (when JSON can carry it) or unserializableValue (a string naming a value JSON cannot carry: "NaN", "Infinity", "-Infinity", "-0", or a bigint literal such as "1n"). JavaScript null is the one primitive CDP reports with type: "object".
  • remoteToJS is the Chrome backend's conversion from a RemoteObject to the JS value passed to the user's callback (or to Bun's console when the option is globalThis.console).

CDP sends null as a {type:"object", subtype:"null"} RemoteObject and
NaN, Infinity, -Infinity and -0 as {type:"number", unserializableValue}
with no value field. remoteToJS handed the console callback the raw
null wrapper and turned all four numbers into 0. Map both to the
primitive they stand for, matching the documented behavior of the
console option.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0bda612d-eb0e-4cc2-9402-0a574b43e750

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6398 and 29d339e.

📒 Files selected for processing (2)
  • src/runtime/webview/ChromeBackend.cpp
  • test/js/bun/webview/webview-chrome.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced on the released build (1.4.0) with Chrome 151 on Linux: a page-side console.log(null, NaN, -0, Infinity, -Infinity) reaches the console callback as { type: "object", subtype: "null", value: null }, 0, 0, 0, 0.

With this branch the callback receives null, NaN, -0, Infinity, -Infinity, and console: globalThis.console prints null NaN Infinity -Infinity -0.

Tests: test/js/bun/webview/webview-chrome.test.ts (new console callback unwraps args whose RemoteObject has no value field, plus an exact stdout assertion in the globalThis.console test). Both fail on the unfixed build and pass with the fix; the full file passes locally.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — targeted fix in remoteToJS mapping CDP's unserializableValue numbers and subtype:"null" to their actual JS values.

Checked: the four unserializableValue strings match the CDP RemoteObject spec, with NaN as the safe fallback; the subtype check is placed before the generic object JSONParse so non-null objects (arrays, dates, plain objects) still fall through unchanged; getString on a missing key returns empty so the == "null" compare is safe. The new test asserts the full arg list including Object.is(-0), and the tightened globalThis.console test now asserts exact stdout, covering the ConsoleClient path. The separate Method::RuntimeEvaluate result decoder is intentionally untouched per the PR description.

Extended reasoning...

Overview

This PR fixes value unwrapping in the Chrome WebView backend's Runtime.consoleAPICalled handler (remoteToJS in src/runtime/webview/ChromeBackend.cpp). Two cases were mishandled: (1) type:"number" RemoteObjects with no value field (NaN, ±Infinity, -0) fell through getDouble("value").value_or(0) and became 0; (2) type:"object", subtype:"null" fell into the generic object branch and reached the callback as a JSONParsed descriptor instead of null. The fix adds an unserializableValue string switch for the number branch and a subtype == "null" early return before the object fallthrough. <limits> is added for std::numeric_limits<double>::infinity(). Tests add a new case asserting the full callback arg list for these values and tighten the existing globalThis.console subprocess test from toContain to an exact toBe on stdout.

Security risks

None. This is pure value-mapping of already-parsed WTF::JSON fields into JS primitives on the console-forwarding path. No user-controlled paths, no allocation sizing from untrusted lengths, no auth/crypto.

Level of scrutiny

Low-to-medium. The change is ~13 lines confined to one lambda in the console event handler; the surrounding ThrowScope/RETURN_IF_EXCEPTION structure is unchanged. jsNumber, jsNaN, and jsNull don't throw, and getString/getDouble on WTF::JSON::Object are non-throwing C++ accessors, so no new exception-check obligations. The generic object/function branch and all other primitive branches are byte-identical to before.

Other factors

No CODEOWNERS cover these files. No prior human review comments to address. Tests follow the file's existing conventions (same chrome backend const, await using, subprocess with bunEnv/bunExe, drained stdout/stderr/exited concurrently). The PR description explicitly scopes out evaluate()'s separate Method::RuntimeEvaluate decoder, which is a distinct code path with a different documented contract, so the fix is correctly not applied there. robobun independently reproduced the before/after behavior on a released build.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:05 PM PT - Aug 15th, 2026

❌ @robobun, your commit 29d339e has some failures in Build #98131 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 39064

That installs a local version of the PR into your bun-39064 executable, so you can run:

bun-39064 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant