LinearRouter matches an empty path segment and returns an empty param
Author: harshit-d3vCreated Sep 12, 2026Updated Sep 12, 2026
LinearRouter matches a path with an empty segment and hands the handler an empty param. The other three routers return 404.
import { Hono } from 'hono'
import { LinearRouter } from 'hono/router/linear-router'
const app = new Hono({ router: new LinearRouter() })
app.get('/u/:id/posts', (c) => c.json({ id: c.req.param('id') }))
await app.request('http://localhost/u/42/posts') // 200 { id: "42" }
await app.request('http://localhost/u//posts') // 200 { id: "" } <- expected 404| Router | /u//posts |
|---|---|
| RegExpRouter | 404 |
| TrieRouter | 404 |
| PatternRouter | 404 |
| LinearRouter | 200, id is "" |
A handler that expects an id to be present gets an empty string instead.
Cause
In match(), the plain label branch already rejects an empty segment at the end of the path but not one in the middle:
let endValuePos = path.indexOf('/', pos + 1)
if (endValuePos === -1) {
if (pos + 1 === path.length) {
continue ROUTES_LOOP // trailing empty segment, rejected
}
endValuePos = path.length
}
value = path.slice(pos + 1, endValuePos)For /u//posts the indexOf returns pos + 1, so value is '' and the match carries on. That is why /a/:p correctly 404s on /a/ but /u/:id/posts matches /u//posts.
Fix is to reject endValuePos === pos + 1 the same way. 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