Skip to content

fix(router): route("/", sub) mounts at the root, not double-slash paths - #98

Open
espetro wants to merge 1 commit into
mainfrom
fix/router-root-mount
Open

espetro wants to merge 1 commit into
mainfrom
fix/router-root-mount

Conversation

@espetro

@espetro espetro commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Summary

app.route("/", sub) registered the sub-router's paths with a doubled slash (//health), so every root-mounted route fell through to the catch-all and 404'd. This PR normalizes a "/" prefix to "" in route() before mounting, matching Hono's app.route("/", sub) behavior.

Closes #97

Root cause

mountRoutes concatenates prefix and sub-path verbatim (packages/router/src/index.ts):

let path = String(prefix) + a;   // prefix "/" + "/health" => "//health"
if (path === "") {
  path = "/";
}

The path === "" guard only covers the empty-prefix case. In route(), the trailing-slash strip is gated on p.length > 1, so a lone "/" prefix survived verbatim and fell through to //health for every root-mounted route.

Repro

import { createRouter } from "@chttp/router";

const sub = createRouter();
sub.get("/health", (c) => c.text("ok"));

const app = createRouter();
app.route("/", sub);
await app.fetch(new Request("http://t/health"));   // 404 pre-fix — registered as "//health"

Pre-fix the repro prints 404; post-fix 200. app.route("", sub) worked before and is unchanged.

The fix

In route(), collapse "/" to "" ahead of the existing trailing-slash strip:

let p = String(prefix);
if (p === "/") {
  p = "";
} else if (p.length > 1 && p.charAt(p.length - 1) === "/") {
  p = String(p.slice(0, p.length - 1));
}

mountRoutes' empty-path guard then turns "" + "" into "/" for root handlers, and route("/api", sub) / route("", sub) behavior is untouched. The change stays inside the scriptc compilable subset (string equality + slice, same primitives as the existing strip); scripts/scriptc-gate.sh still reports 100% static coverage on all gates.

Tests

Added to packages/router/test/router.test.js (tests written first; the three root-mount cases fail pre-fix):

  1. route("/", sub) resolves a sub-route (/health → handler runs, 200).
  2. route("/", sub) resolves a handler registered at "/" in the sub-app (/ → 200).
  3. Param preservation through the root mount (/item/:id).
  4. route("/api", sub) still mounts under the prefix (no regression).

pnpm --filter @chttp/router build && pnpm --filter @chttp/router test: 105/105 pass. Full workspace pnpm -r test: all packages and examples pass, 0 failures.

Context

Found while porting Brioso's web API (briosoco/brioso#859) from Hono to chttp: miscApp mounts at the root and its /health, /ready, /robots.txt, /sitemap.xml routes all 404'd because of this bug.

Changeset

.changeset/router-root-mount.md — @chttp/router patch intent; pnpm change check passes.

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.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying chttpjs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2ec6765
Status: ✅  Deploy successful!
Preview URL: https://f7eca125.chttpjs.pages.dev
Branch Preview URL: https://fix-router-root-mount.chttpjs.pages.dev

View logs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(router): app.route("/", sub) registers double-slash paths — root-mounted sub-routers 404

1 participant