From 2ec6765031e18fa3b1fc148743aea0d62629a82f Mon Sep 17 00:00:00 2001 From: Quino Terrasa <26749475+espetro@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:49:45 +0200 Subject: [PATCH] fix(router): route("/", sub) mounts at the root, not double-slash paths mountRoutes() concatenates prefix + sub-path verbatim, so a "/" prefix registered root-mounted sub-router routes as "//health"-style paths and every root-mounted route 404'd. route() now normalizes a "/" prefix to "" before mounting; mountRoutes' existing empty-path guard still maps "" + "" to "/" for root handlers. "/api" and "" prefix behavior unchanged. Adds tests covering a root-mounted sub-route, a sub-handler at "/", param preservation through the root mount, and a "/api" prefix regression check. --- .changeset/router-root-mount.md | 5 ++++ packages/router/src/index.ts | 8 +++++- packages/router/test/router.test.js | 38 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/router-root-mount.md diff --git a/.changeset/router-root-mount.md b/.changeset/router-root-mount.md new file mode 100644 index 0000000..c5bd9a6 --- /dev/null +++ b/.changeset/router-root-mount.md @@ -0,0 +1,5 @@ +--- +"@chttp/router": patch +--- + +fix(router): `route("/", sub)` now mounts the sub-app at the root instead of registering double-slash paths (`//health`) that 404 on every request. A `"/"` prefix collapses to `""` before `mountRoutes` concatenation, matching Hono behavior. diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 79c0b58..22dbc56 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -1698,7 +1698,13 @@ export class App { route(prefix: string, sub: App): App { let p = String(prefix); - if (p.length > 1 && p.charAt(p.length - 1) === "/") { + // A "/" prefix mounts at the root (issue #97): mountRoutes concatenates + // prefix + sub-path verbatim, so "/" must collapse to "" instead of + // registering "//health"-style double-slash paths. Its empty-path guard + // then turns "" + "" into "/" for root handlers. + if (p === "/") { + p = ""; + } else if (p.length > 1 && p.charAt(p.length - 1) === "/") { p = String(p.slice(0, p.length - 1)); } mountRoutes(sub, this, p, copyMw(this._mw)); diff --git a/packages/router/test/router.test.js b/packages/router/test/router.test.js index 497ca3b..d9f8d4f 100644 --- a/packages/router/test/router.test.js +++ b/packages/router/test/router.test.js @@ -313,6 +313,44 @@ test("route() mounts a sub-app under a prefix preserving params", () => { assert.equal(call(app, { url: "/api/" }).body, "subroot"); }); +test('route("/", sub) mounts sub-app routes at the root (issue #97)', () => { + const sub = createRouter(); + sub.get("/health", (c) => c.text("ok")); + const app = createRouter(); + app.route("/", sub); + const res = call(app, { url: "/health" }); + assert.equal(res.statusCode, 200); + assert.equal(res.body, "ok"); +}); + +test('route("/", sub) resolves a handler registered at "/" in the sub-app', () => { + const sub = createRouter(); + sub.get("/", (c) => c.text("subroot")); + const app = createRouter(); + app.route("/", sub); + const res = call(app, { url: "/" }); + assert.equal(res.statusCode, 200); + assert.equal(res.body, "subroot"); +}); + +test('route("/", sub) preserves params through the root mount', () => { + const sub = createRouter(); + sub.get("/item/:id", (c) => c.json({ id: c.req.param("id") })); + const app = createRouter(); + app.route("/", sub); + assert.equal(JSON.parse(call(app, { url: "/item/7" }).body).id, "7"); + assert.equal(call(app, { url: "/missing" }).statusCode, 404); +}); + +test('route("/api", sub) still mounts under the prefix after the root fix', () => { + const sub = createRouter(); + sub.get("/users", (c) => c.text("users")); + const app = createRouter(); + app.route("/api", sub); + assert.equal(call(app, { url: "/api/users" }).body, "users"); + assert.equal(call(app, { url: "/users" }).statusCode, 404); +}); + test("route() inherits middleware from both sides", () => { const hits = []; const sub = createRouter();