Environment
- Nitro
3.0.260610-beta (current latest), also reproduces on main @ e36e7a6
- Node.js v25.5.0, macOS arm64
- Default preset (
node-server), no other config
Reproduction
Minimal, four files:
// nitro.config.ts
import { defineConfig } from "nitro";
export default defineConfig({
compatibilityDate: "2026-08-24",
handlers: [
{ route: "/api/**", handler: "./server/api-auth.ts", middleware: true },
{ route: "/api/hello", handler: "./server/hello.ts" },
],
});
// server/api-auth.ts
export default (event: any) => {
console.log("middleware ran for", event.url.pathname);
};
// server/hello.ts
export default () => ({ message: "hello" });
npx nitro build
node .output/server/index.mjs
curl -i http://localhost:3000/api/hello
Result: HTTP 500, body {"error":true,"status":500,"unhandled":true}. The middleware never
runs and the route handler is never reached.
Removing the middleware: true entry and leaving only /api/hello returns 200 {"message":"hello"}.
Describe the bug
Registering a handler with middleware: true and a route pattern — the route-scoped middleware
form documented at https://nitro.build/docs/routing#route-scoped-middleware — makes every request
matching that route fail with TypeError: fn is not a function.
The generated routing module and the generated app disagree about the shape of a routed-middleware
match. findRoutedMiddleware serializes each match with serializeHandler, which emits a route
record:
https://github.com/nitrojs/nitro/blob/main/src/build/virtual/routing.ts#L51
export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandler, matchAll: true })};
so each match's data is { route, method, meta, handler }. The generated app then passes that
object straight to h3 as middleware:
https://github.com/nitrojs/nitro/blob/main/src/build/virtual/app.ts#L171
` middleware.push(...findRoutedMiddleware(method, pathname).map((r) => r.data));`
and h3 calls it as a function, which throws.
Global middleware is not affected: it is serialized with serializeHandlerFn in the same file,
which emits the bare handler rather than a route record.
The mismatch is present at least as far back as d37caec (2025-12-11); I did not bisect further.
There is currently no test covering route-scoped middleware, which is likely why it went unnoticed.
I have a fix — a one-word change in routing.ts to use serializeHandlerFn for routed middleware,
plus a regression test in the fixture that asserts the route-rule → global → routed ordering. Happy
to open the PR.
Logs
TypeError: fn is not a function
at callMiddleware (.output/server/_libs/h3+rou3+srvx.mjs:1254:14)
at next (.output/server/_libs/h3+rou3+srvx.mjs:1251:16)
at callMiddleware (.output/server/_libs/h3+rou3+srvx.mjs:1255:36)
at H3Core.handler (.output/server/_libs/h3+rou3+srvx.mjs:1312:34)
at ~request (.output/server/_libs/h3+rou3+srvx.mjs:1321:29)
at H3Core.fetch (.output/server/_libs/h3+rou3+srvx.mjs:1302:26)
at appHandler (.output/server/index.mjs:178:16)
at Server.handler (.output/server/_libs/h3+rou3+srvx.mjs:877:16)
status: 500,
unhandled: true
Environment
3.0.260610-beta(currentlatest), also reproduces onmain@e36e7a6node-server), no other configReproduction
Minimal, four files:
Result:
HTTP 500, body{"error":true,"status":500,"unhandled":true}. The middleware neverruns and the route handler is never reached.
Removing the
middleware: trueentry and leaving only/api/helloreturns200 {"message":"hello"}.Describe the bug
Registering a handler with
middleware: trueand a route pattern — the route-scoped middlewareform documented at https://nitro.build/docs/routing#route-scoped-middleware — makes every request
matching that route fail with
TypeError: fn is not a function.The generated routing module and the generated app disagree about the shape of a routed-middleware
match.
findRoutedMiddlewareserializes each match withserializeHandler, which emits a routerecord:
https://github.com/nitrojs/nitro/blob/main/src/build/virtual/routing.ts#L51
so each match's
datais{ route, method, meta, handler }. The generated app then passes thatobject straight to h3 as middleware:
https://github.com/nitrojs/nitro/blob/main/src/build/virtual/app.ts#L171
` middleware.push(...findRoutedMiddleware(method, pathname).map((r) => r.data));`and h3 calls it as a function, which throws.
Global middleware is not affected: it is serialized with
serializeHandlerFnin the same file,which emits the bare handler rather than a route record.
The mismatch is present at least as far back as
d37caec(2025-12-11); I did not bisect further.There is currently no test covering route-scoped middleware, which is likely why it went unnoticed.
I have a fix — a one-word change in
routing.tsto useserializeHandlerFnfor routed middleware,plus a regression test in the fixture that asserts the route-rule → global → routed ordering. Happy
to open the PR.
Logs
TypeError: fn is not a function at callMiddleware (.output/server/_libs/h3+rou3+srvx.mjs:1254:14) at next (.output/server/_libs/h3+rou3+srvx.mjs:1251:16) at callMiddleware (.output/server/_libs/h3+rou3+srvx.mjs:1255:36) at H3Core.handler (.output/server/_libs/h3+rou3+srvx.mjs:1312:34) at ~request (.output/server/_libs/h3+rou3+srvx.mjs:1321:29) at H3Core.fetch (.output/server/_libs/h3+rou3+srvx.mjs:1302:26) at appHandler (.output/server/index.mjs:178:16) at Server.handler (.output/server/_libs/h3+rou3+srvx.mjs:877:16) status: 500, unhandled: true