Repository: unjs/h3
Pull request #839 (2024-07-21 18:06:44)

perf: reduce the cost of global middleware
Running global and scoped middleware has a cost. This PR slightly reduces the cost by using promise chains.

Additional fixes:
- router context is now available for routed middleware
- global middleware match method if specified





Patch:
diff --git a/src/h3.ts b/src/h3.ts
index 2781313..59492a1 100644
--- a/src/h3.ts
+++ b/src/h3.ts
@@ -133,31 +133,37 @@ class _H3 implements H3 {
     // 2. Global middleware
     const _middleware = this._middleware;
     if (_middleware) {
-      _chain = (_chain || Promise.resolve()).then(async () => {
-        for (const entry of _middleware) {
-          const result = await entry.handler(event);
-          if (result !== undefined && result !== kNotFound) {
-            return result;
+      _chain = _chain || Promise.resolve();
+      for (const m of _middleware) {
+        _chain = _chain.then((_previous) => {
+          if (_previous !== undefined && _previous !== kNotFound) {
+            return _previous;
           }
-        }
-      });
+          if (m.method && m.method !== event.request.method) {
+            return;
+          }
+          return m.handler(event);
+        });
+      }
     }
 
     // 3. Middleware router
     const _mRouter = this._mRouter;
     if (_mRouter) {
-      _chain = (_chain || Promise.resolve()).then(async (_previous) => {
-        if (_previous !== undefined && _previous !== kNotFound) {
-          return _previous;
-        }
-        const matches = findAllRoutes(_mRouter, event.request.method, pathname);
+      const matches = findAllRoutes(_mRouter, event.request.method, pathname);
+      if (matches.length > 0) {
+        _chain = _chain || Promise.resolve();
         for (const match of matches) {
-          const result = await match.data.handler(event);
-          if (result !== undefined && result !== kNotFound) {
-            return result;
-          }
+          _chain = _chain.then((_previous) => {
+            if (_previous !== undefined && _previous !== kNotFound) {
+              return _previous;
+            }
+            event.context.params = match.params;
+            event.context.matchedRoute = match.data;
+            return match.data.handler(event);
+          });
         }
-      });
+      }
     }
 
     // 4. Route handler


Test patch:
diff --git a/test/bench/bench.test.ts b/test/bench/bench.test.ts
index 2de122a..c42dace 100644
--- a/test/bench/bench.test.ts
+++ b/test/bench/bench.test.ts
@@ -15,7 +15,6 @@ describe("benchmark", async () => {
               body: request.body,
             }),
           );
-          expect(response.status).toBe(200);
           if (request.response.body) {
             expect(await response.text()).toBe(request.response.body);
           }
@@ -26,6 +25,7 @@ describe("benchmark", async () => {
               expect(response.headers.get(key)).toBe(value);
             }
           }
+          expect(response.status).toBe(200);
         });
       }
     }
diff --git a/test/bench/impl.ts b/test/bench/impl.ts
index 99e3d62..d9fdfe3 100644
--- a/test/bench/impl.ts
+++ b/test/bench/impl.ts
@@ -1,9 +1,11 @@
 import * as _h3src from "../../src";
 import * as _h3v1 from "h3-v1";
+import * as _h3nightly from "h3-nightly";
 
 export function createInstances() {
   return [
     ["h3", h3(_h3src)],
+    // ["h3-middleware", h3Middleware(_h3src)],
     // ["h3-v1", h3v1()],
     ["maximum", fastest()],
   ] as const;
@@ -27,6 +29,28 @@ export function h3(lib: typeof _h3src) {
   return app.fetch;
 }
 
+export function h3Middleware(lib: typeof _h3src) {
+  const app = lib.createH3();
+
+  // Global middleware
+  app.use(() => {});
+  app.use(() => Promise.resolve());
+
+  // [GET] /
+  app.use("/", () => "Hi");
+
+  // [GET] /id/:id
+  app.use("/id/:id", (event) => {
+    event.response.setHeader("x-powered-by", "benchmark");
+    return `${event.context.params!.id} ${event.url.searchParams.get("name")}`;
+  });
+
+  // [POST] /json
+  app.use("/json", (event) => event.request.json());
+
+  return app.fetch;
+}
+
 export function h3v1() {
   const router = _h3v1.createRouter();
   const app = _h3v1.createApp();
diff --git a/test/router.test.ts b/test/router.test.ts
index 47a2089..047cc4a 100644
--- a/test/router.test.ts
+++ b/test/router.test.ts
@@ -235,9 +235,9 @@ describe("event.context.matchedRoute", () => {
   });
 
   describe("without router", () => {
-    it("can return `undefined` for matched path", async () => {
+    it("middleware can access matched route", async () => {
       ctx.app.use("/**", (event) => {
-        expect(event.context.matchedRoute).toEqual(undefined);
+        expect(event.context.matchedRoute).toMatchObject({ route: "/**" });
         return "200";
       });
       const result = await ctx.request.get("/test/path");
diff --git a/test/web.test.ts b/test/web.test.ts
index 4e60c81..e163fa0 100644
--- a/test/web.test.ts
+++ b/test/web.test.ts
@@ -48,7 +48,7 @@ describe("Web handler", () => {
       query: {
         test: "123",
       },
-      contextKeys: ["test"],
+      contextKeys: ["test", "params", "matchedRoute"],
     });
   });
 });