nitrojs / nitrojs/nitro

bun preset: `event.node.req` is a non-readable unenv mock — raw body proxying (`fetch(url, { body: event.node.req })`) throws `Readable.asyncIterator is not implemented yet!`, and manual stream wraps read an empty body

Open
#4,604 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

pending triage
Dominant language
TypeScript
Stars
11.2k
Forks
899
Avg merge
2d 24m
Merged PRs (30d)
40

Description

Environment
  • nitropack 2.13.4 (latest), unenv 2.0.0-rc.24
  • Bun 1.3.14, 1.4.2, and 1.4.3 (the 1.4.3 run reproduced by a Bun maintainer — see Additional context)
  • Reproduced on macOS arm64, Debian (Docker), and Linux x64
  • No custom nitro config — NITRO_PRESET=bun nitro build, served with bun .output/server/index.mjs
  • Works correctly with NITRO_PRESET=node-server under Node — bun preset only
Reproduction

Inline below rather than a StackBlitz/CodeSandbox link: the starters run Node WebContainers and cannot execute the Bun runtime this bug requires. It is three tiny files with no external services, and it has already been reproduced independently by a Bun maintainer from this exact snippet (Bun 1.4.3, Linux x64 — link in Additional context).

package.json

{
  "name": "nitro-bun-raw-proxy-repro",
  "private": true,
  "dependencies": { "nitropack": "^2.13.4" }
}

routes/proxy.post.ts

// Standard raw pass-through: stream the incoming body to an upstream
// (the usual BFF pattern for multipart uploads).
export default defineEventHandler(async (event) => {
  const res = await fetch('http://localhost:9999/echo', {
    method: 'POST',
    headers: { 'content-type': 'text/plain' },
    body: event.node.req as unknown as ReadableStream,
    // @ts-expect-error duplex is required for stream bodies
    duplex: 'half'
  })
  return { upstreamStatus: res.status, upstreamBody: await res.text() }
})

upstream.ts

Bun.serve({
  port: 9999,
  async fetch(req) {
    const text = await req.text()
    return Response.json({ receivedBytes: text.length })
  }
})
console.log('upstream on 9999')

Run:

bun install
NITRO_PRESET=bun bun node_modules/.bin/nitro build
bun upstream.ts &
PORT=9998 bun .output/server/index.mjs &
curl -s -X POST http://localhost:9998/proxy -H 'content-type: text/plain' --data 'hello-from-repro'

Actual: {"error":true,"url":"http://localhost:9998/proxy","statusCode":500,...} with Readable.asyncIterator is not implemented yet! in the server log (see Logs).

Expected (and what NITRO_PRESET=node-server + Node returns for the identical project): {"upstreamStatus":200,"upstreamBody":"{\"receivedBytes\":16}"}.

Describe the bug

Under the bun preset, event.node.req is unenv's mock Readable, not a readable stream. The preset runtime (nitropack/dist/presets/bun/runtime/bun.mjs) reads the whole body up front with req.arrayBuffer() and passes the buffer to nitroApp.localFetch(...), which builds event.node.req from unenv's mock IncomingMessage. The mock:

  • hardcodes readableEnded = true
  • never emits data
  • throws from Symbol.asyncIterator on first call

So any raw body pass-through fails in one of two ways:

  1. Loudfetch(upstream, { body: event.node.req, duplex: 'half' }): fetch consumes the body via the async iterator, the stub throws, the route 500s, and the upstream request is never sent. Runtime-agnostic: undici consumes async-iterable bodies the same way, so the mock is unreadable by design — a Bun maintainer verified Bun's fetch streams a real node:stream Readable and a real http.IncomingMessage body correctly.
  2. Silent (worse) — wrapping event.node.req in a manual web ReadableStream (data/end listeners plus a readableEnded pre-check): the wrapper sees readableEnded === true, closes immediately, and forwards a 0-byte body. Silent data loss — the upstream receives {"receivedBytes":0} for a 16-byte request.

Expected: event.node.req behaves as a readable carrying the request body, as it does under node-server. The body demonstrably exists in the preset runtime — it was already buffered by req.arrayBuffer() — it just is not wired into the node-compat request. Either side could fix it: the bun preset hands the buffered body to a real Readable, or the unenv mock iterates over the buffered body instead of throwing.

A deploy-path note on impact: builds inside oven/bun images resolve node to the image's bun-node fallback shim, so nitro auto-selects the bun preset even when the build command looks Node-based — teams can be on this preset without knowing it, and the silent variant means raw proxies can appear to work while dropping every uploaded byte.

Additional context
  • First filed against Bun and closed as not-planned after a maintainer ran this repro and confirmed the analysis above (Bun 1.4.3, Linux x64): <BUN_ISSUE_URL>. Their conclusion: "Bun cannot read a body from an object that throws when it is read. Please report this to nitrojs/nitro (the bun preset could hand the buffered body to a real Readable) or unjs/unenv (the mock could iterate over the buffered body)."

  • The bundled mock, at the throw site in .output/server/index.mjs:

    function o(n){throw new Error(`${n} is not implemented yet!`)}
    class i extends EventEmitter{__unenv__={};readableEnded=true;/* … */
      async*[Symbol.asyncIterator](){throw o("Readable.asyncIterator")} /* … */}
    
  • Workaround that works today: h3's getRequestWebStream(event) — it detects the shimmed request (__unenv__ marker) and returns the raw body h3 buffered, forwarding correctly under both presets and both runtimes.

Logs
[request error] [unhandled] [POST] http://localhost:9998/proxy
error: Readable.asyncIterator is not implemented yet!
      at o (.output/server/index.mjs:590:25)
      at .output/server/index.mjs:590:861

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the buffered request body through nitropack/dist/presets/bun/runtime/bun.mjs and the generated unenv IncomingMessage mock described in .output/server/index.mjs. Reproduce with routes/proxy.post.ts and upstream.ts, then verify that raw body forwarding preserves all bytes and matches the node-server result without triggering Readable.asyncIterator.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.