Skip to content

Route-scoped middleware throws fn is not a function and returns 500 #4557

Description

@ShreeBohara

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions