Multi-service dispatch (bun preset): validate resolved service exports a fetch handler instead of a silent 500 on n.fetch is not a function
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11.2k
- Forks
- 899
- Avg merge
- 2d 24m
- Merged PRs (30d)
- 40
Description
Summary
Nitro's multi-service dispatch (observed via the bun preset, nitro 3.0.260610-beta) dereferences a resolved service module's .fetch without validating that the module is a WinterCG { fetch } handler. When a service is registered against a module that does not export a fetch handler, the result is a raw TypeError: <x>.fetch is not a function, wrapped as an opaque {"error":true,"status":500,"unhandled":true} on every request routed to that service — with no indication of which service or why.
This is a defensive / DX request: fail loudly at wire-up (or first dispatch) with an actionable error naming the offending service and the expected handler shape, instead of a silent 500 that gives consumers nothing to debug.
Root cause of our concrete case is upstream in the consumer (vinext registers the wrong export) — filed at cloudflare/vinext#3197. Nitro is not "wrong" here; it's doing exactly what it always does. But a validation guard would have turned a multi-day root-cause hunt into a one-line error. Filing per that issue's suggestion.
Where
The generated multi-service dispatch wrapper (source form, from a built bun-preset server):
function n(e) { // lazy, memoized service handle
let t, n;
return {
fetch(r) {
return n
? n.fetch(r)
: (t ||= e().then(e => n = e.default || e), // <-- normalises ESM namespace
t.then(e => e.fetch(r))); // <-- assumes `.fetch` exists
}
};
}
// service registered against a module whose chosen export is NOT a {fetch} handler:
var r = { ssr: n(() => import(`../_ssr/entry.mjs`).then(e => e.t)) };
n = e.default || e normalises the module namespace, but nothing checks the result actually has a callable fetch. If it doesn't, n.fetch(r) throws, nitro's prod handler wraps it as unhandled (error.unhandled ?? !HTTPError.isError(error) → true), and the response is the generic 500 envelope. The server-side TypeError is only visible in stderr; the HTTP response carries nothing.
Observed
TypeError: n.fetch is not a function. (In 'n.fetch(r)', 'n.fetch' is undefined)
→ {"error":true,"status":500,"unhandled":true} on every dynamic request.
Requested (defensive validation)
At service resolution (or first dispatch), assert the resolved module exposes a callable fetch and throw a clear, actionable error otherwise, e.g.:
Nitro: service "ssr" resolved to a module that does not export a WinterCG `fetch(request)` handler
(got: object with keys [buildId, handleApiRoute, renderPage, ...], no `fetch`, no `default`).
The service entry must default-export (or export) a { fetch(request): Response } handler.
This does not fix a mis-registered service (that's the consumer's job) — it converts a silent, un-attributable 500 into an immediate, named, debuggable failure. Ideally validate once at wire-up so it fails at build/boot rather than per-request.
Environment
nitro3.0.260610-beta, presetbun, runtime Bun 1.4.0 (also seen on 1.3.14).- Reproduces on the uncompiled
.output/server/index.mjsunderbun(not a downstream compile/bundle artifact). - Consumer:
vinext@1.0.0-beta.8/beta.9(SSR service registration) — the cause is there (cloudflare/vinext#3197); this issue is only about nitro surfacing the failure usefully.
Minimal repro
Any nitro bun-preset build where a registered service module lacks a fetch export reproduces the silent 500. A concrete end-to-end repro (vinext SSR app) is in cloudflare/vinext#3197; the nitro-specific behaviour is the un-validated .fetch dereference above.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the failure with a Bun-preset build and inspect the generated .output/server/index.mjs, especially the multi-service dispatch wrapper and its lazy service resolution. Trace that wrapper to the source that generates it and add validation for a callable fetch handler with the service name in the error. Done means an invalid service produces an actionable named error instead of the generic unhandled 500.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100