Skip to content

docs: IPC does not carry Blob, Bun.file() or net.BlockList across processes - #38494

Open
robobun wants to merge 3 commits into
mainfrom
farm/cbf494d6/ipc-docs-platform-objects
Open

robobun wants to merge 3 commits into
mainfrom
farm/cbf494d6/ipc-docs-platform-objects

Conversation

@robobun

@robobun robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • docs/guides/process/ipc.mdx:57, docs/runtime/child-process.mdx:304 and the ipc / Subprocess.send JSDoc in packages/bun-types/bun.d.ts say advanced-mode IPC supports everything structuredClone supports.
  • It does not: a Blob (including File and Bun.file()) or a net.BlockList inside a message arrives in the other process as {}, while structuredClone of the same value returns a working clone. This came up while auditing the IPC guide.
  • The runtime behavior is deliberate, so the docs are the part that is wrong:
    • CloneSerializer writes ObjectTag + TerminatorTag (an empty object) for any Bun class not marked transferable when serializing for a cross-process transfer (src/jsc/bindings/webcore/SerializedScriptValue.cpp:1405-1411); Blob and BlockList are the two such classes (src/runtime/webcore/response.classes.ts, src/runtime/socket/sockets.classes.ts). Introduced on purpose in js: fix serialization of non-transferable objects #19351.
    • test/js/node/cluster.test.ts ("cloneable and non-transferable not-equals", BunFile and BlockList) and test/js/web/workers/structuredClone-classes.test.ts assert the empty object.
    • Node.js v26.3.0 does the same for these values: Blob, File and net.BlockList sent through child_process IPC arrive as {} in both json and advanced mode (probe below).
  • The same two JSDoc blocks also still say IPC only works with other bun processes. That has been false since serialization: "json" landed: the serialization JSDoc a few lines below, the "IPC between Bun & Node.js" section of child-process.mdx, and test/js/bun/spawn/spawn.ipc.bun-node.test.ts all have Bun talking to a Node child.

Fix

  • Reword the four places to describe IPC in terms of structuredClone and name the Blob/BlockList deviation from it: the values structuredClone accepts arrive with their types intact, the values it rejects make send() throw a DataCloneError (verified for URL, Headers, Response, FormData, AbortSignal, MessagePort and a nested function, from both process.send and subprocess.send), and a Blob / Bun.file() / net.BlockList inside a message arrives as {}, as in Node.js.
  • Replace the "only other bun instances" sentence in the ipc JSDoc with the real constraint (the default "advanced" format can only be read by another bun process; use "json" for Node.js) and drop the same clause from send(), matching the neighbouring disconnect() JSDoc.
  • Add a test to test/js/bun/spawn/spawn.ipc.test.ts that pins the documented contract through Bun.spawn({ ipc }) in both directions: Date / Map / Uint8Array keep their types, URL and a nested function make process.send and subprocess.send throw DataCloneError, and Blob, Bun.file(), net.BlockList and a nested Blob arrive as plain empty objects. Existing coverage only checked Bun.file() through node:cluster. Since this PR changes no runtime code, the test passes before and after it; it exists so a later change to the serializer (for example one of the open PRs that touch cross-process serialization) fails this test and updates the docs along with it.
  • Every value named in the new text was sent through Bun 1.4.0 IPC; the same probes under Node v26.3.0 back the "as in Node.js" claim for the Blob and BlockList rows only, which is the only row it is attached to (Node delivers {} for URL/Headers where Bun throws, so the text does not claim Node parity for those).
  • No runtime change. bun bd test test/js/bun/spawn/spawn.ipc.test.ts passes (14 tests, including the new one); bun test test/integration/bun-types/bun-types.test.ts passes with the JSDoc edits.

Background

  • Bun has two IPC serialization modes. json uses JSON.stringify; advanced (the default for Bun.spawn({ ipc }) and for child_process with serialization: "advanced") uses the structured clone serializer with a cross-process flag set.
  • The structured clone serializer handles Bun's own classes through a per-class hook declared in the .classes.ts files. That declaration also says whether the class may cross a process boundary (transferable). A file-backed Blob serializes as the sending process's path or raw file descriptor number (src/runtime/webcore/blob/Store.rs:177) and a BlockList serializes as a handle to the native object inside the sending process (src/runtime/node/net/BlockList.rs:411), so neither is marked transferable; in-process structuredClone, postMessage and bun:jsc serialization do not set the cross-process flag and still clone them.
  • Everything else in the message takes the same code path as structuredClone, which is why unsupported values throw the same DataCloneError over IPC that they throw in-process.
  • The empty object (rather than a DataCloneError) for the two Bun classes mirrors what V8's serializer produces for these values in Node's child_process IPC.
Probe 1: round-trips. A child sends each value with process.send, the parent prints what it received

Same two files run under both runtimes (bun probe-parent.mjs advanced / node probe-parent.mjs advanced); bunFile is only sent under Bun. Bun 1.4.0 and Node v26.3.0 print the same lines.

string       [object String] "s"
number       [object Number] 1.5
bigint       [object BigInt] 10
nested       [object Object] {"a":[1,{"b":"c"}],"d":null}
date         [object Date] 1970-01-01T00:00:00.000Z
regexp       [object RegExp] /a+/gi
map          [object Map] [["k",1]]
set          [object Set] [1,2]
error        [object Error] boom
typedArray   [object Uint8Array] {"0":1,"1":2}
arrayBuffer  [object ArrayBuffer] [3,4]
blob         [object Object] {}
file         [object Object] {}
blockList    [object Object] {}
nestedBlob   [object Object] {"x":{}}
bunFile      [object Object] {}          (Bun only)
Probe 2: structuredClone vs IPC for platform objects (advanced mode)
Bun 1.4.0
name           structuredClone            ipc send                  received as
blob           [object Blob]              sent                      [object Object] {}
file           [object Blob]              sent                      [object Object] {}
blockList      [object BlockList]         sent                      [object Object] {}
bunFile        [object Blob]              sent                      [object Object] {}
url            throws DataCloneError      throws DataCloneError
headers        throws DataCloneError      throws DataCloneError
response       throws DataCloneError      throws DataCloneError
formData       throws DataCloneError      throws DataCloneError
abortSignal    throws DataCloneError      throws DataCloneError
messagePort    throws DataCloneError      throws DataCloneError
domException   [object DOMException]      sent                      [object DOMException]

Node v26.3.0
blob / file / blockList                   sent                      [object Object] {}
url / headers / response / formData       sent                      [object Object] {}

subprocess.send() from a Bun.spawn({ ipc }) parent behaves the same as process.send() above: URL and a nested function throw DataCloneError, a nested Blob is delivered as {"b":{}}.

…cesses

The IPC guide, the child process reference and the Bun.spawn JSDoc said
advanced-mode IPC supports everything structuredClone supports. Platform
objects are deliberately not sent across the process boundary: the
receiving process gets an empty object in their place, which is also
what Node.js does. Say so, and list the values that do round-trip.
@robobun
robobun requested a review from alii as a code owner August 14, 2026 12:24
@coderabbitai

coderabbitai Bot commented Aug 14, 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: 2 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: 72bfb371-5c3c-440a-9ee1-65d49cdacd95

📥 Commits

Reviewing files that changed from the base of the PR and between 032b8db and 4b47582.

📒 Files selected for processing (4)
  • docs/guides/process/ipc.mdx
  • docs/runtime/child-process.mdx
  • packages/bun-types/bun.d.ts
  • test/js/bun/spawn/spawn.ipc.test.ts

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

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: docs, JSDoc and a test pinning the documented behavior; no runtime change. Ready for a maintainer (packages/bun-types needs a code owner). No open review threads.

What is in the PR now:

  • The docs and the two JSDoc blocks describe advanced IPC as structuredClone with the Blob / Bun.file() / net.BlockList deviation (those arrive as {}, as in Node.js); the stale "only other bun processes" sentences are replaced with the real constraint ("advanced" is bun-to-bun, use "json" for Node.js).
  • test/js/bun/spawn/spawn.ipc.test.ts gained a test that sends the documented values through Bun.spawn({ ipc }) in both directions. It passes before and after this PR (nothing in the runtime changes); it is there so a future serializer change has to update the docs with it.

How the documented behavior was verified:

  • Every value named in the text was sent through IPC with Bun 1.4.0 (process.send from a child and subprocess.send from a Bun.spawn({ ipc }) parent) and the same probes were run under Node v26.3.0; both probe outputs are in the PR description.
  • The {} behavior is the one introduced on purpose in js: fix serialization of non-transferable objects #19351 and asserted by test/js/node/cluster.test.ts and test/js/web/workers/structuredClone-classes.test.ts.
  • bun bd test test/js/bun/spawn/spawn.ipc.test.ts (14 pass) and bun test test/integration/bun-types/bun-types.test.ts (15 pass) on this branch; the edited files are prettier-clean.

CI on the earlier revisions: every job that ran passed. Build 95987 was marked failed because the two darwin 14 aarch64 test shards expired in the queue without ever getting an agent, and build 96424 lost two Windows jobs to Azure agent provisioning errors (recorded in its robobun annotation). Neither lane exercises anything in this diff.

The guides audit in #38441 does not touch either of the two doc lines changed here.

@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 — docs-only correction to the IPC serialization description.

What was reviewed:

  • Verified the claim against SerializedScriptValue.cpp:1405-1411: non-transferable Bun classes are written as ObjectTag + TerminatorTag (empty object) when SerializationForCrossProcessTransfer::Yes.
  • Confirmed test/js/node/cluster.test.ts asserts {} for BunFile and BlockList over IPC, matching the new prose.
  • The three edited locations (two .mdx guides, two JSDoc blocks in bun.d.ts) use identical wording and preserve the existing MDN link and "no ownership transfer" caveat.
Extended reasoning...

Overview

This PR rewords one sentence in each of three places — docs/guides/process/ipc.mdx, docs/runtime/child-process.mdx, and two JSDoc blocks in packages/bun-types/bun.d.ts — that previously claimed advanced-mode IPC "supports everything structuredClone supports". The new text lists concrete types that do round-trip and states that platform objects (Blob, Bun.file(), net.BlockList) arrive as {}, matching Node.js. No runtime, type-signature, or test changes.

Security risks

None. Prose and JSDoc comments only; no executable code, no type surface changes (the send(message: any): void signature is untouched).

Level of scrutiny

Low. This is a documentation accuracy fix. I verified the underlying runtime behavior directly: CloneSerializer at src/jsc/bindings/webcore/SerializedScriptValue.cpp:1405-1411 writes ObjectTag + TerminatorTag for any StructuredCloneableSerialize class that isn't isForTransfer when the cross-process flag is set, and test/js/node/cluster.test.ts has two tests ("cloneable and non-transferable not-equals" for BunFile and BlockList) asserting the empty-object result. The PR description also includes an empirical probe showing Bun and Node v26.3.0 produce identical output, backing the "as it does in Node.js" claim.

Other factors

The wording is consistent across all three locations, keeps the MDN structured-clone link, and retains the existing "does not support transferring ownership" sentence. The list of supported types (strings, numbers, plain objects/arrays, Date, RegExp, Map, Set, Error, typed arrays, ArrayBuffer) matches what the JSC serializer handles and what the author's probe confirmed round-trips. No prior reviews or outstanding comments on the PR.

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:48 AM PT - Aug 14th, 2026

@robobun, your commit 6c50ea8 is building: #96424

…the bun-only claim

Values structuredClone rejects (URL, Headers, functions) make send() throw
a DataCloneError over IPC too, so "platform objects arrive as {}" was too
broad; only Blob (File, Bun.file()) and net.BlockList degrade to an empty
object. Scope the sentence to values inside a message, since handles are
passed through a separate argument.

The two JSDoc blocks also still said IPC only works with other bun
processes, which the serialization option right below them contradicts.

@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.

I reviewed this PR and didn't find any bugs — the four reworded passages accurately describe the cross-process serializer's behavior. Deferring only because packages/bun-types/bun.d.ts is CODEOWNER-covered, so a human sign-off is required.

What was reviewed

  • Verified the empty-object claim against SerializedScriptValue.cpp (ObjectTag+TerminatorTag for non-transferable Bun classes under SerializationForCrossProcessTransfer::Yes) and the transferable: false declarations for Blob and BlockList in their .classes.ts files.
  • Confirmed the .d.ts edits are JSDoc-only; no type signatures changed.
  • Checked the "only other bun instances" removal is consistent with the existing serialization JSDoc and the Bun↔Node IPC docs section.
Extended reasoning...

Overview

This PR rewords one sentence each in docs/guides/process/ipc.mdx and docs/runtime/child-process.mdx, and updates two JSDoc blocks (ipc option and Subprocess.send) in packages/bun-types/bun.d.ts. All four edits replace the claim that advanced-mode IPC "supports everything structuredClone supports" with a more accurate description: values structuredClone accepts arrive intact, values it rejects throw DataCloneError, and Blob/Bun.file()/net.BlockList arrive as {} (matching Node.js). The .d.ts change also drops the stale "only compatible with other bun instances" claim in favor of pointing at serialization: "json" for Node interop.

Security risks

None. Pure prose/JSDoc; no runtime code, no type signatures, no build inputs.

Level of scrutiny

Low. The behavior being documented is deliberate (introduced in #19351), asserted by existing tests (test/js/node/cluster.test.ts, test/js/web/workers/structuredClone-classes.test.ts), and I verified the mechanism directly in SerializedScriptValue.cpp:1405-1411 — non-transferable Bun classes are written as ObjectTag+TerminatorTag when m_forTransfer == SerializationForCrossProcessTransfer::Yes. Both Blob (response.classes.ts) and BlockList (sockets.classes.ts) declare transferable: false. The "as in Node.js" qualifier is scoped only to the Blob/BlockList row, which the PR's probe output supports.

Other factors

The .d.ts edits touch only comment text — no interface members, parameter types, or return types change — so the bun-types integration test is unaffected (and the PR reports it passing). The removed "another instance of bun" clause was already contradicted by the neighbouring serialization JSDoc and the "IPC between Bun & Node.js" docs section, so this brings the JSDoc into line with the rest of the docs. I would have approved outright, but packages/bun-types/ is listed in .github/CODEOWNERS, so per policy I'm deferring for the owner's sign-off.

Values structuredClone clones keep their types, values it rejects make
send() throw DataCloneError in both directions, and Blob, Bun.file() and
net.BlockList arrive as empty objects. The existing coverage only checked
Bun.file() through node:cluster; this covers Bun.spawn in both directions
so the docs and the serializer cannot drift apart silently.

@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 — docs/JSDoc corrections plus a test that pins the described contract; no runtime code touched.

What was reviewed:

  • Verified the doc claim against SerializedScriptValue.cpp:1405-1411 — cross-process serialization of a non-isForTransfer Bun class writes ObjectTag+TerminatorTag (empty object), and response.classes.ts marks Blob transferable: false.
  • Checked the new test for flakiness: onMessage is armed before spawn() and before each send(), onExit is wired to reject the raced promise, and exitedEarly.catch(() => {}) prevents an unhandled rejection when the child exits normally at the end.
  • The "only other bun instances" JSDoc removal is consistent with the neighbouring serialization JSDoc and the existing bun↔node IPC test.
Extended reasoning...

Overview

This PR touches four files with no runtime code changes:

  • Two docs pages (docs/guides/process/ipc.mdx, docs/runtime/child-process.mdx) — one sentence each, rewording the advanced-mode IPC description from "supports everything structuredClone supports" to the accurate contract: structuredClone-accepted values keep their types, structuredClone-rejected values throw DataCloneError, and Blob/Bun.file()/net.BlockList degrade to {}.
  • packages/bun-types/bun.d.ts — the same rewording in the ipc option and Subprocess.send() JSDoc, plus replacing the stale "only compatible with other bun instances" claim with the real constraint (advanced needs bun↔bun; use "json" for Node.js).
  • test/js/bun/spawn/spawn.ipc.test.ts — a new test that exercises every branch of the documented contract in both directions (child→parent and parent→child).

Security risks

None. This is documentation and a read-only test; no runtime, parser, or serializer code is modified.

Level of scrutiny

Low-to-medium. The docs edits are prose only and I confirmed the underlying behavior against the source: SerializedScriptValue.cpp:1405-1411 writes ObjectTag+TerminatorTag for any StructuredCloneableSerialize whose isForTransfer is false when SerializationForCrossProcessTransfer::Yes, and response.classes.ts:150 sets transferable: false for Blob. The "as in Node.js" clause is scoped only to the Blob/BlockList sentence, matching the probe output in the PR description (Node delivers {} for URL/Headers where Bun throws, so parity is not claimed there).

The new test warranted a closer read for the flakiness patterns REVIEW.md calls out. It awaits real conditions (no sleeps), wires onExit into a rejected promise raced against every awaited message so an early child crash surfaces as a clear failure, arms onMessage before each send so no message can be lost to a race, registers exitedEarly.catch(() => {}) up front so the deliberate exit at the end does not produce an unhandled rejection, uses await using for cleanup, and asserts exact object shapes plus constructor identity for the degraded values. If process.send(URL) unexpectedly succeeded instead of throwing, the extra message would resolve fromChild with the wrong shape and the toEqual on kind: "from-child" would fail — so the DataCloneError assertion is not vacuous.

Other factors

  • The behavior being documented is already asserted by test/js/node/cluster.test.ts and test/js/web/workers/structuredClone-classes.test.ts per the PR description; this PR adds direct Bun.spawn IPC coverage in the file that owns that surface.
  • The .d.ts change is JSDoc-only (no type signatures altered), so the bun-types integration test is the relevant gate and the PR reports it passing.
  • No prior human or claude[bot] reviews on this PR; only bot status comments.

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