From 56edebf2e0bde6f5bd01ca08aff0a34a22ca1a35 Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Thu, 14 May 2026 01:10:51 -0400 Subject: [PATCH 1/3] fix(bun): polyfill ReadableStream to strip unsupported type "direct" React 19's server.edge.js creates ReadableStream({ type: "direct", ... }) using a Cloudflare Workers-specific extension. Bun follows the web spec strictly and throws ERR_INVALID_ARG_VALUE for unknown type values, breaking prerender builds with the bun preset. Add a global ReadableStream wrapper in the bun runtime entry that strips the type property when its value is "direct" before delegating to the native constructor. Prototype chain is preserved so instanceof checks pass. Fixes #4259 --- src/presets/bun/runtime/bun.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index f9983c0c7e..c454a0be0f 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -1,4 +1,36 @@ import "#nitro/virtual/polyfills"; + +// React 19's server.edge.js uses ReadableStream({ type: "direct", ... }), a +// Cloudflare Workers extension. Bun follows the web spec strictly and throws +// ERR_INVALID_ARG_VALUE for unknown `type` values. Strip it before it reaches +// Bun's constructor so prerendering works without switching to the node preset. +const _OriginalReadableStream = globalThis.ReadableStream; +// @ts-expect-error -- intentional global override for compat +globalThis.ReadableStream = function ReadableStream( + underlyingSource?: UnderlyingDefaultSource | UnderlyingByteSource, + strategy?: QueuingStrategy, +) { + if ( + underlyingSource && + (underlyingSource as Record).type === "direct" + ) { + const { type: _type, ...rest } = underlyingSource as Record; + return new _OriginalReadableStream( + rest as UnderlyingDefaultSource, + strategy, + ); + } + return new _OriginalReadableStream( + underlyingSource as UnderlyingDefaultSource, + strategy, + ); +} as unknown as typeof ReadableStream; +Object.setPrototypeOf(globalThis.ReadableStream, _OriginalReadableStream); +Object.setPrototypeOf( + globalThis.ReadableStream.prototype, + _OriginalReadableStream.prototype, +); + import type { ServerRequest } from "srvx"; import { serve } from "srvx/bun"; import wsAdapter from "crossws/adapters/bun"; From 62f94cf99e9d13522d64286cd2e935e5b8e48fd8 Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Fri, 15 May 2026 09:13:42 -0400 Subject: [PATCH 2/3] fix(bun): preserve ReadableStream prototype in polyfill and add test --- src/presets/bun/runtime/bun.ts | 14 +++++++------- test/fixture/server/routes/bun-direct-stream.ts | 15 +++++++++++++++ test/presets/bun.test.ts | 7 ++++++- 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 test/fixture/server/routes/bun-direct-stream.ts diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index c454a0be0f..7e0e1a7793 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -5,8 +5,7 @@ import "#nitro/virtual/polyfills"; // ERR_INVALID_ARG_VALUE for unknown `type` values. Strip it before it reaches // Bun's constructor so prerendering works without switching to the node preset. const _OriginalReadableStream = globalThis.ReadableStream; -// @ts-expect-error -- intentional global override for compat -globalThis.ReadableStream = function ReadableStream( +const _PatchedReadableStream = function ReadableStream( underlyingSource?: UnderlyingDefaultSource | UnderlyingByteSource, strategy?: QueuingStrategy, ) { @@ -25,11 +24,12 @@ globalThis.ReadableStream = function ReadableStream( strategy, ); } as unknown as typeof ReadableStream; -Object.setPrototypeOf(globalThis.ReadableStream, _OriginalReadableStream); -Object.setPrototypeOf( - globalThis.ReadableStream.prototype, - _OriginalReadableStream.prototype, -); + +Object.setPrototypeOf(_PatchedReadableStream, _OriginalReadableStream); +_PatchedReadableStream.prototype = _OriginalReadableStream.prototype; + +// @ts-expect-error -- intentional global override for compat +globalThis.ReadableStream = _PatchedReadableStream; import type { ServerRequest } from "srvx"; import { serve } from "srvx/bun"; diff --git a/test/fixture/server/routes/bun-direct-stream.ts b/test/fixture/server/routes/bun-direct-stream.ts new file mode 100644 index 0000000000..c78461eb78 --- /dev/null +++ b/test/fixture/server/routes/bun-direct-stream.ts @@ -0,0 +1,15 @@ +export default () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + // @ts-expect-error - direct is a Cloudflare extension + type: "direct", + start(controller) { + controller.enqueue(encoder.encode("bun-direct")); + controller.close(); + }, + }); + + return { + isStream: stream instanceof ReadableStream, + }; +}; diff --git a/test/presets/bun.test.ts b/test/presets/bun.test.ts index 1b5bbe556e..df25396498 100644 --- a/test/presets/bun.test.ts +++ b/test/presets/bun.test.ts @@ -1,7 +1,7 @@ import { execa, execaCommandSync } from "execa"; import { getRandomPort, waitForPort } from "get-port-please"; import { resolve } from "pathe"; -import { describe } from "vitest"; +import { describe, it, expect } from "vitest"; import { setupTest, testNitro } from "../tests.ts"; const hasBun = execaCommandSync("bun --version", { stdio: "ignore", reject: false }).exitCode === 0; @@ -25,5 +25,10 @@ describe.runIf(hasBun)("nitro:preset:bun", async () => { const res = await ctx.fetch(url, opts); return res; }; + }, (ctx, callHandler) => { + it("bun: ReadableStream polyfill works", async () => { + const { data } = await callHandler({ url: "/bun-direct-stream" }); + expect(data.isStream).toBe(true); + }); }); }); From d100d4c93406a8807e77c9dab4c9e16aeced2a9e Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Fri, 15 May 2026 09:21:50 -0400 Subject: [PATCH 3/3] fix(bun): use class extends for ReadableStream polyfill to preserve instanceof --- src/presets/bun/runtime/bun.ts | 36 +++++++++++++++------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index 7e0e1a7793..04975f8461 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -4,29 +4,25 @@ import "#nitro/virtual/polyfills"; // Cloudflare Workers extension. Bun follows the web spec strictly and throws // ERR_INVALID_ARG_VALUE for unknown `type` values. Strip it before it reaches // Bun's constructor so prerendering works without switching to the node preset. +// Using class extends preserves the prototype chain so instanceof checks work correctly. const _OriginalReadableStream = globalThis.ReadableStream; -const _PatchedReadableStream = function ReadableStream( - underlyingSource?: UnderlyingDefaultSource | UnderlyingByteSource, - strategy?: QueuingStrategy, -) { - if ( - underlyingSource && - (underlyingSource as Record).type === "direct" +class _PatchedReadableStream extends _OriginalReadableStream { + constructor( + underlyingSource?: UnderlyingDefaultSource | UnderlyingByteSource, + strategy?: QueuingStrategy, ) { - const { type: _type, ...rest } = underlyingSource as Record; - return new _OriginalReadableStream( - rest as UnderlyingDefaultSource, - strategy, - ); + if ( + underlyingSource && + (underlyingSource as Record).type === "direct" + ) { + const { type: _type, ...rest } = + underlyingSource as Record; + super(rest as UnderlyingDefaultSource, strategy); + } else { + super(underlyingSource as UnderlyingDefaultSource, strategy); + } } - return new _OriginalReadableStream( - underlyingSource as UnderlyingDefaultSource, - strategy, - ); -} as unknown as typeof ReadableStream; - -Object.setPrototypeOf(_PatchedReadableStream, _OriginalReadableStream); -_PatchedReadableStream.prototype = _OriginalReadableStream.prototype; +} // @ts-expect-error -- intentional global override for compat globalThis.ReadableStream = _PatchedReadableStream;