From b3c42baf7d5db535a6ed9d9eec21c5aabc335837 Mon Sep 17 00:00:00 2001 From: CyberneticX-Tech <138270143+CyberneticX-Tech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:10:46 -0700 Subject: [PATCH 1/3] fix(routing): serialize routed middleware as handler functions Fixes #4557 by serializing routedMiddleware with serializeHandlerFn instead of serializeHandler, ensuring callable middleware functions are registered with h3. --- src/build/virtual/routing.ts | 2 +- test/unit/routed-middleware.test.ts | 64 +++++++++++++++++++++++++++++ test/unit/virtual-routing.test.ts | 33 +++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 test/unit/routed-middleware.test.ts diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index a64087c4a0..5aea9bc922 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -48,7 +48,7 @@ ${allHandlers export const findRoute = ${nitro.routing.routes.compileToString({ serialize: (h) => serializeHandler(h, { tracing: traceH3 }) })} -export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandler, matchAll: true })}; +export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandlerFn, matchAll: true })}; export const globalMiddleware = [ ${nitro.routing.globalMiddleware.map((h) => (h.lazy ? h._importHash : `h3.toEventHandler(${h._importHash})`)).join(",")} diff --git a/test/unit/routed-middleware.test.ts b/test/unit/routed-middleware.test.ts new file mode 100644 index 0000000000..89c67a5a9d --- /dev/null +++ b/test/unit/routed-middleware.test.ts @@ -0,0 +1,64 @@ +import { afterAll, describe, expect, it } from "vitest"; +import { createNitro, build, prepare } from "nitro/builder"; +import { join } from "pathe"; +import { fileURLToPath } from "node:url"; +import { rm, mkdir } from "node:fs/promises"; + +const tmpDir = fileURLToPath(new URL("./.tmp/routed-middleware", import.meta.url)); + +describe("route-scoped middleware", () => { + afterAll(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + it("executes route-scoped middleware without throwing fn is not a function", async () => { + const outDir = join(tmpDir, "output"); + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); + + const nitro = await createNitro({ + rootDir: tmpDir, + preset: "standard", + output: { dir: outDir }, + virtual: { + "#auth-middleware": () => + `export default (event) => { event.res.headers.set("x-auth-middleware", "true"); }`, + "#hello-handler": () => `export default () => ({ message: "hello" })`, + "#other-handler": () => `export default () => ({ message: "other" })`, + }, + handlers: [ + { + route: "/api/**", + handler: "#auth-middleware", + middleware: true, + }, + { + route: "/api/hello", + handler: "#hello-handler", + }, + { + route: "/other", + handler: "#other-handler", + }, + ], + }); + + await prepare(nitro); + await build(nitro); + + const entry = join(outDir, "server/index.mjs"); + const { fetch } = await import(entry).then((m) => m.default); + + // Request matching routed middleware + const res = await fetch(new Request("http://localhost/api/hello")); + expect(res.status).toBe(200); + expect(res.headers.get("x-auth-middleware")).toBe("true"); + expect(await res.json()).toEqual({ message: "hello" }); + + // Request not matching routed middleware + const resOther = await fetch(new Request("http://localhost/other")); + expect(resOther.status).toBe(200); + expect(resOther.headers.get("x-auth-middleware")).toBeNull(); + expect(await resOther.json()).toEqual({ message: "other" }); + }); +}); diff --git a/test/unit/virtual-routing.test.ts b/test/unit/virtual-routing.test.ts index 412453fd73..1f7dd0e72d 100644 --- a/test/unit/virtual-routing.test.ts +++ b/test/unit/virtual-routing.test.ts @@ -52,4 +52,37 @@ describe("virtual/routing template", () => { expect(template).toContain(`import { wrapHandlerWithTracing } from "h3/tracing"`); expect(template).toContain("wrapHandlerWithTracing(h3.toEventHandler(_abc123))"); }); + + it("serializes routed middleware as handler functions instead of route descriptor objects", () => { + const middlewareHandler: NitroEventHandler & { _importHash: string } = { + route: "/api/**", + handler: "/path/to/middleware.ts", + middleware: true, + _importHash: "_mid123", + }; + const nitroStub = { + options: { + tracingChannel: undefined, + baseURL: "/", + routeRules: {}, + }, + routing: { + routes: { + routes: [], + compileToString: () => `{}`, + }, + routedMiddleware: { + routes: [{ route: "/api/**", method: "", data: middlewareHandler }], + compileToString: ({ serialize }: { serialize: (h: unknown) => string }) => + `{"/api/**":${serialize(middlewareHandler)}}`, + }, + globalMiddleware: [], + }, + } as unknown as Nitro; + + const template = routing(nitroStub).template(); + expect(template).toContain("export const findRoutedMiddleware ="); + expect(template).toContain("h3.toEventHandler(_mid123)"); + expect(template).not.toContain(`route:"/api/**"`); + }); }); From 68cc60898eee71043203c9953c9219f34d363a90 Mon Sep 17 00:00:00 2001 From: CyberneticX-Tech <138270143+CyberneticX-Tech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:19:18 -0700 Subject: [PATCH 2/3] fix(routing): support tracing on routed middleware and assert execution ordering --- src/build/virtual/routing.ts | 16 ++++++--- test/unit/routed-middleware.test.ts | 51 +++++++++++++++++++++++++++++ test/unit/virtual-routing.test.ts | 32 ++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index 5aea9bc922..3b7eae0b39 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -48,7 +48,7 @@ ${allHandlers export const findRoute = ${nitro.routing.routes.compileToString({ serialize: (h) => serializeHandler(h, { tracing: traceH3 }) })} -export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandlerFn, matchAll: true })}; +export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: (h) => serializeHandlerFn(h, { tracing: traceH3 }), matchAll: true })}; export const globalMiddleware = [ ${nitro.routing.globalMiddleware.map((h) => (h.lazy ? h._importHash : `h3.toEventHandler(${h._importHash})`)).join(",")} @@ -72,20 +72,23 @@ function serializeHandler( ): string { const meta = Array.isArray(h) ? h[0] : h; const handler = Array.isArray(h) - ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` - : serializeHandlerFn(h); + ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler, opts)).join(",")})` + : serializeHandlerFn(h, opts); return `{${[ `route:${JSON.stringify(meta.route)}`, meta.method && `method:${JSON.stringify(meta.method)}`, meta.meta && `meta:${JSON.stringify(meta.meta)}`, - `handler:${opts.tracing ? `wrapHandlerWithTracing(${handler})` : handler}`, + `handler:${handler}`, ] .filter(Boolean) .join(",")}}`; } -function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): string { +function serializeHandlerFn( + h: NitroEventHandler & { _importHash: string }, + opts: { tracing?: boolean } = {} +): string { let code = h._importHash; if (!h.lazy) { if (h.format === "node") { @@ -93,5 +96,8 @@ function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): str } code = `h3.toEventHandler(${code})`; } + if (opts.tracing) { + code = `wrapHandlerWithTracing(${code})`; + } return code; } diff --git a/test/unit/routed-middleware.test.ts b/test/unit/routed-middleware.test.ts index 89c67a5a9d..fe2a6fbe9a 100644 --- a/test/unit/routed-middleware.test.ts +++ b/test/unit/routed-middleware.test.ts @@ -61,4 +61,55 @@ describe("route-scoped middleware", () => { expect(resOther.headers.get("x-auth-middleware")).toBeNull(); expect(await resOther.json()).toEqual({ message: "other" }); }); + + it("executes middleware in route-rules -> global -> routed middleware order", async () => { + const outDir = join(tmpDir, "output-ordering"); + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); + + const nitro = await createNitro({ + rootDir: tmpDir, + preset: "standard", + output: { dir: outDir }, + routeRules: { + "/api/**": { + headers: { "x-order-1-rule": "rule" }, + }, + }, + virtual: { + "#global-middleware": () => + `export default (event) => { event.res.headers.append("x-order", "global"); }`, + "#routed-middleware": () => + `export default (event) => { event.res.headers.append("x-order", "routed"); }`, + "#order-handler": () => `export default () => ({ status: "ok" })`, + }, + handlers: [ + { + handler: "#global-middleware", + middleware: true, + }, + { + route: "/api/**", + handler: "#routed-middleware", + middleware: true, + }, + { + route: "/api/order", + handler: "#order-handler", + }, + ], + }); + + await prepare(nitro); + await build(nitro); + + const entry = join(outDir, "server/index.mjs"); + const { fetch } = await import(entry).then((m) => m.default); + + const res = await fetch(new Request("http://localhost/api/order")); + expect(res.status).toBe(200); + expect(res.headers.get("x-order-1-rule")).toBe("rule"); + expect(res.headers.get("x-order")).toBe("global, routed"); + expect(await res.json()).toEqual({ status: "ok" }); + }); }); diff --git a/test/unit/virtual-routing.test.ts b/test/unit/virtual-routing.test.ts index 1f7dd0e72d..f05ca4bcef 100644 --- a/test/unit/virtual-routing.test.ts +++ b/test/unit/virtual-routing.test.ts @@ -85,4 +85,36 @@ describe("virtual/routing template", () => { expect(template).toContain("h3.toEventHandler(_mid123)"); expect(template).not.toContain(`route:"/api/**"`); }); + + it("wraps routed middleware with wrapHandlerWithTracing when tracingChannel.h3 is true", () => { + const middlewareHandler: NitroEventHandler & { _importHash: string } = { + route: "/api/**", + handler: "/path/to/middleware.ts", + middleware: true, + _importHash: "_mid123", + }; + const nitroStub = { + options: { + tracingChannel: { h3: true }, + baseURL: "/", + routeRules: {}, + }, + routing: { + routes: { + routes: [], + compileToString: () => `{}`, + }, + routedMiddleware: { + routes: [{ route: "/api/**", method: "", data: middlewareHandler }], + compileToString: ({ serialize }: { serialize: (h: unknown) => string }) => + `{"/api/**":${serialize(middlewareHandler)}}`, + }, + globalMiddleware: [], + }, + } as unknown as Nitro; + + const template = routing(nitroStub).template(); + expect(template).toContain("export const findRoutedMiddleware ="); + expect(template).toContain("wrapHandlerWithTracing(h3.toEventHandler(_mid123))"); + }); }); From 292c0d416249c41d10921c9f4040cad40edbc5d4 Mon Sep 17 00:00:00 2001 From: CyberneticX-Tech <138270143+CyberneticX-Tech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:45:45 -0700 Subject: [PATCH 3/3] fix(routing): use middleware-specific tracing and strengthen execution ordering test --- src/build/virtual/routing.ts | 25 ++++++++++++++++++++----- test/unit/routed-middleware.test.ts | 26 +++++++++++++++++++------- test/unit/virtual-routing.test.ts | 4 ++-- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index 3b7eae0b39..afbf6eac83 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -27,6 +27,18 @@ import * as srvxNode from "srvx/node" import * as h3 from "h3";${traceH3 ? `\nimport { wrapHandlerWithTracing } from "h3/tracing";` : ""} ${routeRulesModule} +${ + traceH3 + ? `const wrapMiddlewareWithTracing = (middleware) => { + const { tracingChannel } = globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {}; + if (!tracingChannel || middleware.__traced__) return middleware; + const channel = tracingChannel("h3.request"); + const wrapped = (...args) => channel.tracePromise(async () => middleware(...args), { event: args[0], type: "middleware" }); + wrapped.__traced__ = true; + return wrapped; +};` + : "" +} const multiHandler = (...handlers) => { const final = handlers.pop() const middleware = handlers.filter(Boolean).map(h => h3.toMiddleware(h)); @@ -48,7 +60,7 @@ ${allHandlers export const findRoute = ${nitro.routing.routes.compileToString({ serialize: (h) => serializeHandler(h, { tracing: traceH3 }) })} -export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: (h) => serializeHandlerFn(h, { tracing: traceH3 }), matchAll: true })}; +export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: (h) => serializeHandlerFn(h, { tracing: traceH3, type: "middleware" }), matchAll: true })}; export const globalMiddleware = [ ${nitro.routing.globalMiddleware.map((h) => (h.lazy ? h._importHash : `h3.toEventHandler(${h._importHash})`)).join(",")} @@ -72,8 +84,8 @@ function serializeHandler( ): string { const meta = Array.isArray(h) ? h[0] : h; const handler = Array.isArray(h) - ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler, opts)).join(",")})` - : serializeHandlerFn(h, opts); + ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler, { ...opts, type: "route" })).join(",")})` + : serializeHandlerFn(h, { ...opts, type: "route" }); return `{${[ `route:${JSON.stringify(meta.route)}`, @@ -87,7 +99,7 @@ function serializeHandler( function serializeHandlerFn( h: NitroEventHandler & { _importHash: string }, - opts: { tracing?: boolean } = {} + opts: { tracing?: boolean; type?: "route" | "middleware" } = {} ): string { let code = h._importHash; if (!h.lazy) { @@ -97,7 +109,10 @@ function serializeHandlerFn( code = `h3.toEventHandler(${code})`; } if (opts.tracing) { - code = `wrapHandlerWithTracing(${code})`; + code = + opts.type === "middleware" + ? `wrapMiddlewareWithTracing(${code})` + : `wrapHandlerWithTracing(${code})`; } return code; } diff --git a/test/unit/routed-middleware.test.ts b/test/unit/routed-middleware.test.ts index fe2a6fbe9a..99361cfa19 100644 --- a/test/unit/routed-middleware.test.ts +++ b/test/unit/routed-middleware.test.ts @@ -73,15 +73,25 @@ describe("route-scoped middleware", () => { output: { dir: outDir }, routeRules: { "/api/**": { - headers: { "x-order-1-rule": "rule" }, + headers: { "x-route-rule": "applied" }, }, }, virtual: { "#global-middleware": () => - `export default (event) => { event.res.headers.append("x-order", "global"); }`, + `export default (event) => { + const current = event.res.headers.get("x-execution-order") || ""; + event.res.headers.set("x-execution-order", current ? current + ", global" : "global"); + }`, "#routed-middleware": () => - `export default (event) => { event.res.headers.append("x-order", "routed"); }`, - "#order-handler": () => `export default () => ({ status: "ok" })`, + `export default (event) => { + const current = event.res.headers.get("x-execution-order") || ""; + event.res.headers.set("x-execution-order", current + ", routed"); + }`, + "#order-handler": () => `export default (event) => ({ + status: "ok", + order: event.res.headers.get("x-execution-order"), + hasContextRouteRules: !!event.context?.routeRules, + })`, }, handlers: [ { @@ -108,8 +118,10 @@ describe("route-scoped middleware", () => { const res = await fetch(new Request("http://localhost/api/order")); expect(res.status).toBe(200); - expect(res.headers.get("x-order-1-rule")).toBe("rule"); - expect(res.headers.get("x-order")).toBe("global, routed"); - expect(await res.json()).toEqual({ status: "ok" }); + expect(res.headers.get("x-execution-order")).toBe("global, routed"); + const data = await res.json(); + expect(data.status).toBe("ok"); + expect(data.order).toBe("global, routed"); + expect(data.hasContextRouteRules).toBe(true); }); }); diff --git a/test/unit/virtual-routing.test.ts b/test/unit/virtual-routing.test.ts index f05ca4bcef..5bff673bd0 100644 --- a/test/unit/virtual-routing.test.ts +++ b/test/unit/virtual-routing.test.ts @@ -86,7 +86,7 @@ describe("virtual/routing template", () => { expect(template).not.toContain(`route:"/api/**"`); }); - it("wraps routed middleware with wrapHandlerWithTracing when tracingChannel.h3 is true", () => { + it("wraps routed middleware with wrapMiddlewareWithTracing when tracingChannel.h3 is true", () => { const middlewareHandler: NitroEventHandler & { _importHash: string } = { route: "/api/**", handler: "/path/to/middleware.ts", @@ -115,6 +115,6 @@ describe("virtual/routing template", () => { const template = routing(nitroStub).template(); expect(template).toContain("export const findRoutedMiddleware ="); - expect(template).toContain("wrapHandlerWithTracing(h3.toEventHandler(_mid123))"); + expect(template).toContain("wrapMiddlewareWithTracing(h3.toEventHandler(_mid123))"); }); });