TrieRouter runs a handler twice when a path segment is literally `*`
A handler runs twice when the last segment of the request path is literally *.
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:
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 twiceCause
In insert() a pattern node is stored twice: under its own key in #children, and in #patterns.
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.
Source: honojs/hono