Summary
When an app overrides ~getMiddleware, per-route middleware is silently skipped — the route handler still runs, but its own middleware never does.
Why
createDispatcher (src/h3.ts) has two paths, and they disagree about who supplies route middleware:
- Default path — the final handler is
routeHandler(route), which composes route.data.middleware in front of route.data.handler.
- Compat path (taken when
~getMiddleware is overridden) — the final handler is the bare route?.data.handler || NoHandler, so route middleware is only present if ~getMiddleware happened to include it.
The default ~getMiddleware does include it, so an override that delegates to super is fine. But an override that returns its own list — which is what the signature invites — drops route middleware with no error.
Repro
const seen: string[] = [];
const app = new H3();
app.use(() => { seen.push("global"); });
app.get("/x", () => "ok", { middleware: [() => { seen.push("route"); }] });
// any custom ~getMiddleware that doesn't re-add route.data.middleware
(app as any)["~getMiddleware"] = function () { return this["~middleware"]; };
await app.request(new Request("http://localhost/x"));
// seen === ["global"] — expected ["global", "route"]
// response body is still "ok", so the handler ran without its middleware
Impact
Mainly integrators that override ~getMiddleware for per-event middleware (nitro is the documented reason the branch exists). If per-route middleware is used for auth or validation, it stops being enforced while the route keeps serving.
Note
Found while reviewing route rules — not a rules bug, this reproduces with plain H3. Raising it separately since ~getMiddleware is part of the internal surface downstream depends on.
Summary
When an app overrides
~getMiddleware, per-routemiddlewareis silently skipped — the route handler still runs, but its own middleware never does.Why
createDispatcher(src/h3.ts) has two paths, and they disagree about who supplies route middleware:routeHandler(route), which composesroute.data.middlewarein front ofroute.data.handler.~getMiddlewareis overridden) — the final handler is the bareroute?.data.handler || NoHandler, so route middleware is only present if~getMiddlewarehappened to include it.The default
~getMiddlewaredoes include it, so an override that delegates tosuperis fine. But an override that returns its own list — which is what the signature invites — drops route middleware with no error.Repro
Impact
Mainly integrators that override
~getMiddlewarefor per-event middleware (nitro is the documented reason the branch exists). If per-route middleware is used for auth or validation, it stops being enforced while the route keeps serving.Note
Found while reviewing route rules — not a rules bug, this reproduces with plain
H3. Raising it separately since~getMiddlewareis part of the internal surface downstream depends on.