#3908·fresh

fsRoutes mounted more than once runs every _middleware once per mount

Author: heavybullets8Created Sep 18, 2026Updated Sep 18, 2026

Fresh 2.3.3, also present on main at 86d6cde.

When the file-system routes are mounted more than once, for example a preview prefix plus the plain tree, every _middleware.ts runs once per mount on a single request. Routes are prefixed correctly; only the middleware, layout and error placement lose the prefix.

Reproduction

deno.json imports: fresh jsr:@fresh/[email protected], @fresh/plugin-vite 1.1.2, vite 7.3.6, preact 10.29.7.

main.ts

typescript
import { App } from "fresh";

export const app = new App()
  .fsRoutes("/a/:id")
  .fsRoutes("/b/:id")
  .fsRoutes();

routes/_middleware.ts

typescript
import type { Middleware } from "fresh";

export const handler: Middleware<unknown> = (ctx) => {
  console.log("middleware ran for " + new URL(ctx.req.url).pathname);
  return ctx.next();
};

routes/index.tsx renders a heading.

bash
deno task dev
curl -s -o /dev/null http://localhost:8000/

Output:

middleware ran for /
middleware ran for /
middleware ran for /

Expected: one line.

Where

packages/fresh/src/commands.ts, applyCommandsInner. The Route case builds its path with mergePath(basePath, pattern), and the FsRoute case recurses with mergePath(basePath, cmd.pattern, true) as the new base. The Middleware, Layout and Error cases call getOrCreateSegment(root, cmd.pattern, ...) with the un-prefixed pattern, so every mount lands its middleware in the same segment and segmentToMiddlewares collects all of them for each route.

Passing the merged base into getOrCreateSegment for those three cases is what I would expect, but I have not tested that change against the suite.