cloudflare / cloudflare/vinext

nitro bun preset: SSR service dispatch resolves the vinext SSR entry to a non-handler → TypeError: n.fetch is not a function (500 unhandled) on every dynamic request

Open
#3,197 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Environment / versions

| package | version |
|---|---|
| `vinext` | `1.0.0-beta.8` |
| `nitro` | `3.0.260610-beta` |
| `vite` | `8.2.2` |
| `@vitejs/plugin-rsc` | `0.5.34` (also repro'd on `0.5.26`) |
| `react` / `react-dom` / `react-server-dom-webpack` | `19.2.6` |
| `next` (compat target) | `16.2.0` |
| runtime | Bun (nitro `preset: 'bun'`) |

Reproduces on the **uncompiled** `.output/server/index.mjs` run directly under `bun` — i.e. it is
the nitro `bun` preset output itself, not any downstream compile/bundle step.

## Summary

With the nitro `bun` preset, **every dynamic request** (SSR page or API route) returns nitro's prod
error envelope:

```json
{"error":true,"status":500,"unhandled":true}
```

The underlying throw is:

```
TypeError: n.fetch is not a function. (In 'n.fetch(r)', 'n.fetch' is undefined)
```

Static routes are fine; anything that dispatches to the SSR service throws. A **no-middleware,
plain `getServerSideProps` page** fails identically, so this is not middleware-, header-, or
API-specific — it is the SSR-service dispatch path.

## Minimal reproduction

A `pages/`-router app with a single SSR page is enough. No middleware required.

**`package.json`**
```json
{
"name": "vinext-ssr-repro",
"private": true,
"type": "module",
"scripts": { "build": "vite build" },
"dependencies": {
"@vitejs/plugin-rsc": "0.5.34",
"nitro": "3.0.260610-beta",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-server-dom-webpack": "19.2.6",
"vinext": "1.0.0-beta.8",
"vite": "8.2.2"
},
"devDependencies": { "@vitejs/plugin-react": "6.1.1" }
}
```

**`vite.config.mjs`**
```js
import { nitro } from 'nitro/vite';
import vinext from 'vinext';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
vinext(),
nitro({ preset: 'bun' }),
],
});
```

**`pages/index.tsx`** (plain SSR, no middleware)
```tsx
export function getServerSideProps() {
return { props: { received: 'hello world' } };
}
export default function Home({ received }: { received: string }) {
return

Received: {received}
;
}
```

**Build & serve**
```bash
npm install
NODE_ENV=production npx vite build # produces .output/server/index.mjs (exit 0)
bun .output/server/index.mjs & # boots, listens
curl -s http://localhost:3000/ # -> {"error":true,"status":500,"unhandled":true}
```

The server-side console prints the `TypeError` and stack (see below). The build **succeeds**; the
failure is purely at request time.

> Notes:
> - `"type": "module"` is required only so the `vite build` succeeds at all — a CommonJS
> `package.json` fails earlier at build with the separate `UNRESOLVED_IMPORT` error (see "Related"
> below). Once the build passes, the runtime `n.fetch` throw is what this issue is about.
> - `inlineDynamicImports: true` in the nitro `rollupConfig.output` (a common consumer setting)
> changes nothing — repro'd both with and without it.

## Expected vs actual

- **Expected:** `GET /` returns `200` with `

Received: hello world
`.
- **Actual:** `GET /` returns `500` with body `{"error":true,"status":500,"unhandled":true}`; the
server logs `TypeError: n.fetch is not a function`.

## The exact failing wrapper + stack frame

The SSR-service dispatch wrapper (source form, from
`.output/server/_chunks/ssr-renderer.mjs`):

```js
// lazy, memoized service handle
function n(e) {
let t, n;
return {
fetch(r) {
return n
? n.fetch(r)
: (t ||= e().then(e => n = e.default || e), // <-- ESM default-interop pick
t.then(e => e.fetch(r)));
}
};
}

// SSR service registered against the vinext SSR entry, selecting its `t` export:
var r = { ssr: n(() => import(`../_ssr/entry.mjs`).then(e => e.t)) };

function i(n, i, a) {
let o = r[n];
if (!o) throw e.status(404);
return Promise.resolve(o.fetch(t(i, a))); // dispatches every dynamic request here
}
```

`n = e.default || e` is meant to normalise an ESM module namespace to its handler. But the value it
resolves — the vinext SSR entry's `t` export (`_ssr/entry.mjs` exports `... ee as t ...`) — is
**not** a WinterCG `{ fetch }` handler and has no `.default`, so `n` ends up as an object with no
`.fetch`, and `n.fetch(r)` throws.

**Actual runtime error + stack** (uncompiled `.output/server/index.mjs` under bun):

```
TypeError: n.fetch is not a function. (In 'n.fetch(r)', 'n.fetch' is undefined)
at fetch (.output/server/index.mjs:90:27835)
at Wy (.output/server/index.mjs:90:28061)
at s (.output/server/index.mjs:5:185) // nitro request pipeline
at ~request (.output/server/index.mjs:5:4624)
at (.output/server/index.mjs:4:1901)
```

nitro then wraps the non-`HTTPError` throw as `{error:true,status:500,unhandled:true}`
(`nitro/dist/runtime/internal/error/prod.mjs`: `unhandled = error.unhandled ?? !HTTPError.isError(error)`).

## Reproduced across every variable we could toggle

Same `TypeError` / same 500 in all of:

- **uncompiled** `bun .output/server/index.mjs` (`n.fetch is not a function`),
- **no-middleware** plain-SSR app (`n.fetch is not a function`),
- `inlineDynamicImports` **off** (nitro code-splitting on) — the wrapper is inlined so it reads
`e.fetch is not a function` instead, same root cause,
- a downstream single-executable **compiled** build (`l.fetch is not a function` — minified alias,
same frame).

The variable that matters is none of these — it is the export shape of the SSR entry that the
service wrapper resolves.

## Likely fix direction

The nitro service dispatcher expects `import(sserEntry).then(m => m.default || m)` to yield an object
with a `fetch(request)` method (WinterCG handler). Either:

1. **(vinext, preferred)** have the vinext SSR entry expose its request handler as the export the
nitro service registration points at — i.e. make the `t` export (or the module default) a real
`{ fetch }` handler — so `e.default || e` yields a value with `.fetch`; **or**
2. **(vinext registration)** register the SSR service against the correct export
(`import('../_ssr/entry.mjs').then(e => e.)`) rather than `e.t`, if the
handler already exists under a different name; **or**
3. **(nitro, defensive)** normalise/validate the resolved service module and fail loudly if it has
no `.fetch`, instead of dereferencing `undefined` — this turns a silent 500 into an actionable
codegen error but does not itself fix the missing handler.

The real fix is (1)/(2): the SSR entry must present a `fetch` export in the shape the nitro service
wrapper resolves.

## Which repo owns this

- The **wrapper** (`n = e.default || e; n.fetch(r)`) is nitro's generic multi-service dispatch
codegen.
- The **contract violation** is on vinext's side: vinext registers the SSR service pointing nitro at
`_ssr/entry.mjs`'s `t` export, but that export is not a `{ fetch }` handler. nitro is doing what it
always does; vinext is handing it a module that doesn't satisfy the service-handler contract.

→ **File primarily against vinext**, cross-reference nitro for the defensive-validation option (3).

## Related (probably the same ESM-default-interop root)

On CommonJS apps (`package.json` without `"type": "module"`), the same toolchain fails earlier at
**build** with:

```
[UNRESOLVED_IMPORT] Could not resolve '../ssr/index.js' in node_modules/.nitro/vite/services/rsc/index.mjs
[UNRESOLVED_IMPORT] Could not resolve '../ssr/index.js' in .../services/rsc/index.mjs
[UNRESOLVED_IMPORT] Could not resolve '../rsc/index.js' in .../services/ssr/index.mjs
```

Both symptoms live at the same rsc↔ssr multi-service seam and both are ESM-interop/module-shape
mismatches between nitro's service split and vinext's rsc/ssr entries — the build one blocks CJS
apps before boot, and this runtime `n.fetch` one blocks ESM apps after boot. They are very likely
two faces of one underlying entry-export/interop defect; worth fixing together.

Contributor guide

Open the contributing guide

Research direction

Reproduce the failure with the minimal pages/ SSR app and inspect the generated .output/server/_chunks/ssr-renderer.mjs and _ssr/entry.mjs. Trace the service registration that selects the t export and compare it with the handler expected by the n.fetch dispatch. Done means dynamic SSR and API requests return successfully, with the related CommonJS build path checked if included in scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, react, typescript, vite
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.