nitrojs / nitrojs/nitro

Multi-service dispatch (bun preset): validate resolved service exports a fetch handler instead of a silent 500 on n.fetch is not a function

Open
#4,606 0 comments 0 reactions 0 assignees View on GitHub

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

  • nitro 3.0.260610-beta, preset bun, runtime Bun 1.4.0 (also seen on 1.3.14).
  • Reproduces on the uncompiled .output/server/index.mjs under bun (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

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.