Repository: unjs/h3 (ts)

Issue:
404 Cannot find any route matching
<!-- issue-up-forward-issue https://github.com/nuxt/nuxt/issues/15293 -->

<table><td><img width="760" height="0" src="https://github.com/nuxt/nuxt/issues/15293">

&nbsp;&nbsp;&nbsp;Forwarded from downstream issue:
- https://github.com/nuxt/nuxt/issues/15293 by @qucode1

</td></table><br>

### Environment

------------------------------
- Operating System: `Darwin`
- Node Version:     `v16.16.0`
- Nuxt Version:     `3.0.0-rc.12`
- Nitro Version:    `0.6.0`
- Package Manager:  `pnpm@6.11.0`
- Builder:          `vite`
- User Config:      `modules`, `ssr`
- Runtime Modules:  `@nuxtjs/tailwindcss@6.1.2`
- Build Modules:    `-`
------------------------------

### Reproduction

https://github.com/qucode1/nuxt-api-route-issue/blob/master/server/api/reset-password.post.ts

### Describe the bug

Server api routes that `return undefined` or nothing at all will run, but the server will return an error that does not help finding the issue:

```ts
export default defineEventHandler(async (event) => {
  try {
    console.log('resetPassword')
    return
  } catch (error) {
    console.log('reset password error', { error })
    return sendError(event, createError({
      ...error,
      statusMessage: error.message,
    }))
  }
})

```

to fix the issue you need to use `event.res.end()`, return null or anything else.

`404 Cannot find any route matching /api/reset-password`
![Bildschirmfoto 2022-10-28 um 18 00 14](https://user-images.githubusercontent.com/28145834/198681843-320b65c1-8ea5-4d39-8ecd-de11ba9ab1c2.png)


### Additional context

_No response_

### Logs

_No response_
    

Pull request description:
fix(router): send 204 with empty string in preemptive mode instead of 404
Resolves #407. When `preemptive` router flag is enabled and an event handler is returning nothing, instead of showing a 404, we response empty string with 204 No Content gracefully.

This PR also makes slight fixes:
 - Use `path` instead of `route` in error message of default app to distinguish errors
 - Respect `preemptive` flag for when a route METHOD is not matching.

Note: For nitro, we need to enable flag to make this change effective.

Patch:
diff --git a/src/app.ts b/src/app.ts
index 475f49a..f49da5a 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -143,7 +143,7 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
     if (!event.node.res.writableEnded) {
       throw createError({
         statusCode: 404,
-        statusMessage: `Cannot find any route matching ${
+        statusMessage: `Cannot find any path matching ${
           event.node.req.url || "/"
         }.`,
       });
diff --git a/src/router.ts b/src/router.ts
index 063c748..5a5d6b6 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -2,6 +2,7 @@ import { createRouter as _createRouter } from "radix3";
 import type { HTTPMethod, EventHandler } from "./types";
 import { createError } from "./error";
 import { eventHandler, toEventHandler } from "./event";
+import { setResponseStatus } from "./utils";
 
 export type RouterMethod = Lowercase<HTTPMethod>;
 const RouterMethods: RouterMethod[] = [
@@ -103,11 +104,15 @@ export function createRouter(opts: CreateRouterOptions = {}): Router {
     ).toLowerCase() as RouterMethod;
     const handler = matched.handlers[method] || matched.handlers.all;
     if (!handler) {
-      throw createError({
-        statusCode: 405,
-        name: "Method Not Allowed",
-        statusMessage: `Method ${method} is not allowed on this route.`,
-      });
+      if (opts.preemptive || opts.preemtive) {
+        throw createError({
+          statusCode: 405,
+          name: "Method Not Allowed",
+          statusMessage: `Method ${method} is not allowed on this route.`,
+        });
+      } else {
+        return; // Let app match other handlers
+      }
     }
 
     // Add params
@@ -115,7 +120,13 @@ export function createRouter(opts: CreateRouterOptions = {}): Router {
     event.context.params = params;
 
     // Call handler
-    return handler(event);
+    return Promise.resolve(handler(event)).then((res) => {
+      if (res === undefined && (opts.preemptive || opts.preemtive)) {
+        setResponseStatus(event, 204);
+        return "";
+      }
+      return res;
+    });
   });
 
   return router;


Test patch:
diff --git a/test/router.test.ts b/test/router.test.ts
index 651444d..6a1f997 100644
--- a/test/router.test.ts
+++ b/test/router.test.ts
@@ -97,11 +97,50 @@ describe("router", () => {
     const res = await request.get("/404");
     expect(res.status).toEqual(404);
   });
+});
+
+describe("router (preemptive)", () => {
+  let app: App;
+  let router: Router;
+  let request: SuperTest<Test>;
+
+  beforeEach(() => {
+    app = createApp({ debug: false });
+    router = createRouter({ preemptive: true })
+      .get(
+        "/test",
+        eventHandler(() => "Test")
+      )
+      .get(
+        "/undefined",
+        eventHandler(() => undefined)
+      );
+    app.use(router);
+    request = supertest(toNodeListener(app));
+  });
+
+  it("Handle /test", async () => {
+    const res = await request.get("/test");
+    expect(res.text).toEqual("Test");
+  });
+
+  it("Handle /404", async () => {
+    const res = await request.get("/404");
+    expect(JSON.parse(res.text)).toMatchObject({
+      statusCode: 404,
+      statusMessage: "Cannot find any route matching /404.",
+    });
+  });
 
   it("Not matching route method", async () => {
     const res = await request.head("/test");
     expect(res.status).toEqual(405);
   });
+
+  it("Handle /undefined", async () => {
+    const res = await request.get("/undefined");
+    expect(res.text).toEqual("");
+  });
 });
 
 describe("getRouterParams", () => {