#5370·hono

TrieRouter runs a handler twice when a path segment is literally `*`

Author: harshit-d3vCreated Sep 12, 2026Updated Sep 14, 2026

A handler runs twice when the last segment of the request path is literally *.

typescript
import { Hono } from 'hono'
import { TrieRouter } from 'hono/router/trie-router'

const app = new Hono({ router: new TrieRouter() })
let calls = 0
app.use('/a/*', async (c, next) => { calls++; await next() })
app.all('*', (c) => c.text('ok'))

await app.request('http://localhost/a/b')   // calls === 1
calls = 0
await app.request('http://localhost/a/*')   // calls === 2

/a/b runs the middleware once. /a/* runs it twice.

This is reachable without choosing TrieRouter explicitly. SmartRouter falls back to it whenever RegExpRouter throws UnsupportedPathError, so this app on the default router hits it too:

typescript
const app = new Hono()
app.use('/a/*', async (c, next) => { calls++; await next() })
app.get('/a/b', (c) => c.text('static'))
app.get('/a/:p', (c) => c.text('param'))
// getRouterName(app) === 'SmartRouter + TrieRouter'
// GET /a/*  ->  middleware runs twice

Cause

In insert() a pattern node is stored twice: under its own key in #children, and in #patterns.

typescript
const child = (curNode.#children[key] ||= new Node())
if (pattern && !child.#pattern) {
  child.#pattern = pattern
  curNode.#patterns.push(child)
}

In search(), a request part of * finds that node through node.#children[part] and pushes its handlers, then the #patterns loop reaches the same node and pushes them again. Any other part misses the #children lookup and only takes the pattern branch, which is why only a literal * duplicates.

The other three routers return the handler once for this path.

I have a fix that skips the pattern branch when it resolves to the node the #children lookup already handled, limited to the last segment so a wildcard still matches the rest of the path. Full suite passes and I added the case to common.case.test.ts, where all four routers pass it. Happy to open the PR.