Skip to content

fix(devtools-vite): avoid piping raw DOM console arguments#424

Open
Dzinlife wants to merge 1 commit intoTanStack:mainfrom
Dzinlife:codex/safe-console-piping-dom-serialization
Open

fix(devtools-vite): avoid piping raw DOM console arguments#424
Dzinlife wants to merge 1 commit intoTanStack:mainfrom
Dzinlife:codex/safe-console-piping-dom-serialization

Conversation

@Dzinlife
Copy link
Copy Markdown

@Dzinlife Dzinlife commented Apr 25, 2026

🎯 Changes

Console piping currently only probes console arguments with JSON.stringify, then sends the original argument when the probe succeeds. DOM nodes such as buttons can pass that probe, so a console warning that includes an element can put the raw DOM object into the payload and make the browser stall while the devtools runtime serializes it.

This PR serializes console arguments into JSON-safe values before sending them through the Vite console pipe:

  • DOM elements become short selector-like strings, for example <button id="save" class="primary" type="button">
  • documents, windows, generic nodes, events, errors, dates, regexes, functions, symbols, bigints, arrays, and circular objects are handled explicitly
  • large payloads are capped by depth, array length, object key count, and string length; binary/typed arrays are summarized instead of expanded
  • original console methods still receive the original arguments

It also adds regression tests for logging DOM elements, circular references, errors, and large payloads through console.warn.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

Ran targeted validation locally:

  • pnpm --filter @tanstack/devtools-vite test:lib -- virtual-console.test.ts
  • pnpm --filter @tanstack/devtools-vite test:types
  • pnpm --filter @tanstack/devtools-vite test:eslint
  • pnpm --filter @tanstack/devtools-vite build
  • pnpm exec prettier --check .changeset/safe-console-dom-pipe.md packages/devtools-vite/src/virtual-console.ts packages/devtools-vite/src/virtual-console.test.ts

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

The pull request improves console argument serialization in the devtools-vite package by introducing a dedicated serializeConsoleArg function that safely converts DOM elements to HTML strings and handles complex types, preventing raw DOM nodes from being piped through the console proxy.

Changes

Cohort / File(s) Summary
Release Metadata
.changeset/safe-console-dom-pipe.md
Added patch release changeset documenting that raw DOM nodes should not be piped through the console proxy.
Virtual Console Implementation
packages/devtools-vite/src/virtual-console.ts
Replaced shallow "safe serialization" with a dedicated serializeConsoleArg function that handles DOM elements (converting to simplified tag markup), special objects (Document, Window, Event), Errors, Dates, RegExp, circular references, and recursively serializes arrays and plain object properties.
Virtual Console Tests
packages/devtools-vite/src/virtual-console.test.ts
Added cleanup hooks to reset Vitest timers and global state after each test, plus a new test verifying that console.warn with DOM element arguments serializes elements to HTML strings before sending via fetch to the SSE endpoint.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant Console as Console Interceptor
    participant Serializer as serializeConsoleArg
    participant Fetch as Fetch/SSE Endpoint

    Client->>Console: console.warn(domElement, ...)
    Console->>Serializer: serialize each argument
    Serializer->>Serializer: detect type (DOM, Error, Date, etc.)
    Serializer->>Serializer: convert to safe representation
    Serializer-->>Console: serialized arguments
    Console->>Fetch: POST request body with serialized args
    Fetch-->>Console: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A console so wise, now sees the light,
DOM nodes transform to strings so bright,
No raw elements in the pipe today,
Serialization clears the way!
Circular paths and Events too,
The devtools now knows what to do. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: avoiding piping raw DOM console arguments through the devtools console proxy.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively follows the required template with detailed changes explanation, completed checklist items, validated local testing, and proper changeset generation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Dzinlife Dzinlife marked this pull request as ready for review April 25, 2026 13:19
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/devtools-vite/src/virtual-console.ts (2)

177-184: Optional: prefer Object.keys over for...in + hasOwnProperty.

for (var key in arg) walks the prototype chain and is then filtered by hasOwnProperty.call. Object.keys(arg) returns the same set of own enumerable string keys directly and is the more idiomatic choice here (and avoids surprises with prototype-polluted globals).

♻️ Proposed change
     var objectResult = {};
-    for (var key in arg) {
-      if (Object.prototype.hasOwnProperty.call(arg, key)) {
-        objectResult[key] = serializeConsoleArg(arg[key], seen);
-      }
-    }
+    var ownKeys = Object.keys(arg);
+    for (var ki = 0; ki < ownKeys.length; ki++) {
+      var key = ownKeys[ki];
+      objectResult[key] = serializeConsoleArg(arg[key], seen);
+    }
     seen.pop();
     return objectResult;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/virtual-console.ts` around lines 177 - 184, The
loop building objectResult in serializeConsoleArg currently uses for...in +
hasOwnProperty which walks the prototype chain; change it to iterate
Object.keys(arg).forEach or a for..of over Object.keys(arg) so only own
enumerable keys are processed, e.g., replace the for (var key in arg) { if
(Object.prototype.hasOwnProperty.call(arg, key)) { ... } } with direct iteration
of Object.keys(arg) and keep the same serializeConsoleArg(arg[key], seen)
assignment and seen.pop() logic unchanged.

162-185: Consider depth/breadth caps to fully achieve the stall-prevention goal.

The current serializer recurses without any depth or length limits. This still leaves a few easy ways to produce huge payloads or stall the page — e.g., a Uint8Array/Buffer of length N falls through to the object branch (since Array.isArray returns false) and for...in materializes N numeric-indexed properties. Deeply nested config-like objects can also blow the stack or balloon the body sent to fetch.

Since the PR's goal is to avoid stalling on serialization, capping MAX_DEPTH, array length, object key count, and string length would close those remaining holes.

♻️ Sketch of caps + TypedArray short-circuit
-  function serializeConsoleArg(arg, seen) {
+  var MAX_DEPTH = 6;
+  var MAX_ARRAY_LEN = 100;
+  var MAX_OBJECT_KEYS = 100;
+
+  function serializeConsoleArg(arg, seen, depth) {
+    if (depth === undefined) depth = 0;
     if (arg === undefined) return 'undefined';
     if (arg === null) return null;
@@
     if (argType !== 'object') return arg;
@@
+    if (depth >= MAX_DEPTH) return '[MaxDepth]';
+
+    if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(arg)) {
+      return '[' + (arg.constructor && arg.constructor.name || 'TypedArray') + '(' + arg.length + ')]';
+    }
+
     if (seen.indexOf(arg) !== -1) {
       return '[Circular]';
     }
     seen.push(arg);

     if (Array.isArray(arg)) {
       var arrayResult = [];
-      for (var arrayIndex = 0; arrayIndex < arg.length; arrayIndex++) {
-        arrayResult.push(serializeConsoleArg(arg[arrayIndex], seen));
+      var arrayLen = Math.min(arg.length, MAX_ARRAY_LEN);
+      for (var arrayIndex = 0; arrayIndex < arrayLen; arrayIndex++) {
+        arrayResult.push(serializeConsoleArg(arg[arrayIndex], seen, depth + 1));
       }
+      if (arg.length > MAX_ARRAY_LEN) arrayResult.push('… (' + (arg.length - MAX_ARRAY_LEN) + ' more)');
       seen.pop();
       return arrayResult;
     }

     var objectResult = {};
-    for (var key in arg) {
-      if (Object.prototype.hasOwnProperty.call(arg, key)) {
-        objectResult[key] = serializeConsoleArg(arg[key], seen);
-      }
-    }
+    var keys = Object.keys(arg);
+    var keyLimit = Math.min(keys.length, MAX_OBJECT_KEYS);
+    for (var ki = 0; ki < keyLimit; ki++) {
+      objectResult[keys[ki]] = serializeConsoleArg(arg[keys[ki]], seen, depth + 1);
+    }
+    if (keys.length > MAX_OBJECT_KEYS) objectResult['…'] = '(' + (keys.length - MAX_OBJECT_KEYS) + ' more keys)';
     seen.pop();
     return objectResult;
   }

Recursive call sites at lines 136-137 and 171/180 would also need to thread depth + 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/virtual-console.ts` around lines 162 - 185, The
serializer serializeConsoleArg currently recurses without limits; add caps to
prevent huge/stalling payloads by introducing MAX_DEPTH, MAX_ITEMS (for
arrays/objects), and MAX_STRING_LENGTH constants, add a depth parameter (default
0) that is incremented at each recursive call, and return a truncated marker
like "[MaxDepth]" or truncated strings when depth exceeds MAX_DEPTH or items
exceed MAX_ITEMS; also short-circuit binary/typed arrays (detect ArrayBuffer,
SharedArrayBuffer, and TypedArray/Buffer) to return a summarized placeholder
instead of iterating their numeric properties, and ensure every recursive call
site (including where serializeConsoleArg(arg[arrayIndex], seen) and object
property calls) passes depth + 1 and enforces item and string caps.
packages/devtools-vite/src/virtual-console.test.ts (1)

80-133: Solid regression test for DOM serialization.

Capturing console.warn before evaluating the generated IIFE so the mock is bound as originalConsole.warn, then asserting it was invoked with the raw button element is a clean way to confirm both that the original logger receives unmodified args and that the piped payload uses the serialized form.

Optional: consider a follow-up test for circular references and Error to lock in those branches of serializeConsoleArg, since they're new behaviors not covered elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/virtual-console.test.ts` around lines 80 - 133,
Add tests that cover serializeConsoleArg's circular-reference and Error
branches: create a new test (e.g., "serializes circular refs and Error objects
before sending logs") that follows the same pattern as the existing serializes
DOM elements test—use vi.useFakeTimers(), stub global fetch and EventSource,
capture original console.warn, call new
Function(generateConsolePipeCode(['warn'], TEST_VITE_URL))(), then invoke
console.warn with (1) an object containing a circular reference and (2) an Error
instance; advance timers, assert originalWarnMock got the raw arguments, assert
fetch was called and parse init.body to verify body.entries contain serialized
representations for the circular object (no throws, handles "[Circular]" or
chosen format) and a serialized Error (name, message, stack or message),
referencing serializeConsoleArg, generateConsolePipeCode, fetchMock and
originalWarnMock to locate code paths to validate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/devtools-vite/src/virtual-console.test.ts`:
- Around line 80-133: Add tests that cover serializeConsoleArg's
circular-reference and Error branches: create a new test (e.g., "serializes
circular refs and Error objects before sending logs") that follows the same
pattern as the existing serializes DOM elements test—use vi.useFakeTimers(),
stub global fetch and EventSource, capture original console.warn, call new
Function(generateConsolePipeCode(['warn'], TEST_VITE_URL))(), then invoke
console.warn with (1) an object containing a circular reference and (2) an Error
instance; advance timers, assert originalWarnMock got the raw arguments, assert
fetch was called and parse init.body to verify body.entries contain serialized
representations for the circular object (no throws, handles "[Circular]" or
chosen format) and a serialized Error (name, message, stack or message),
referencing serializeConsoleArg, generateConsolePipeCode, fetchMock and
originalWarnMock to locate code paths to validate.

In `@packages/devtools-vite/src/virtual-console.ts`:
- Around line 177-184: The loop building objectResult in serializeConsoleArg
currently uses for...in + hasOwnProperty which walks the prototype chain; change
it to iterate Object.keys(arg).forEach or a for..of over Object.keys(arg) so
only own enumerable keys are processed, e.g., replace the for (var key in arg) {
if (Object.prototype.hasOwnProperty.call(arg, key)) { ... } } with direct
iteration of Object.keys(arg) and keep the same serializeConsoleArg(arg[key],
seen) assignment and seen.pop() logic unchanged.
- Around line 162-185: The serializer serializeConsoleArg currently recurses
without limits; add caps to prevent huge/stalling payloads by introducing
MAX_DEPTH, MAX_ITEMS (for arrays/objects), and MAX_STRING_LENGTH constants, add
a depth parameter (default 0) that is incremented at each recursive call, and
return a truncated marker like "[MaxDepth]" or truncated strings when depth
exceeds MAX_DEPTH or items exceed MAX_ITEMS; also short-circuit binary/typed
arrays (detect ArrayBuffer, SharedArrayBuffer, and TypedArray/Buffer) to return
a summarized placeholder instead of iterating their numeric properties, and
ensure every recursive call site (including where
serializeConsoleArg(arg[arrayIndex], seen) and object property calls) passes
depth + 1 and enforces item and string caps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7301ded-7d58-4613-8271-a3ee674de38a

📥 Commits

Reviewing files that changed from the base of the PR and between f1844a2 and 8d202e6.

📒 Files selected for processing (3)
  • .changeset/safe-console-dom-pipe.md
  • packages/devtools-vite/src/virtual-console.test.ts
  • packages/devtools-vite/src/virtual-console.ts

@Dzinlife Dzinlife force-pushed the codex/safe-console-piping-dom-serialization branch from 8d202e6 to cd077e3 Compare April 25, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant