Fragment identifiers cause incorrect route parameter parsing in Service Worker
What version of Hono are you using?
4.9.9
What runtime/platform is your app running on? (with version if possible)
browser service worker (Chrome)
What steps can reproduce the bug?
1: Run Hono in a Service Worker context using fire() from hono/service-worker
2: Define routes
app.get('/users/', (c) => c.html(...))
app.get('/users/:id', (c) => c.html(...))
3: User navigates to the page with a fragment:
GET /users/#user-list
GET /users/1#profile-section
What is the expected behavior?
/users/#user-listmatches/users/route/users/1#profile-sectionmatches/users/:idroute withid = "1"
What do you see instead?
/users/#user-listmatches/users/:idroute withid = "#user-list"/users/1#profile-sectionmatches/users/:idbut withid = "1#profile-section"
This occurs because Service Workers receive the full URL including fragments, unlike traditional web servers where browsers strip fragments before sending requests. Hono's route matcher treats # as a regular character.
Additional information
No response
I attempted to work around this issue by reconstructing the Request object with the fragment removed. however, this fails with the error:
TypeError: Failed to construct 'Request': Cannot construct a Request with a RequestInit whose mode member is set as 'navigate'.
Hi @yatsuna827, thanks for your report.
I see, that's certainly how it works with the service worker pattern. For the current Hono implementation, I believe the following workaround is appropriate.
const app = new Hono({
getPath: (req) => new URL(req.url).pathname
})
I'm a bit torn about whether we should modify Hono's implementation to address this.
I think the following options are available.
- Do nothing
- Keep it as is, letting users specify it via the
new Hono()options
- Keep it as is, letting users specify it via the
- Modify the default
getPath() - Perform some special processing in the service worker handler
Modify the default getPath()
In typical server-side applications, # is never passed as an argument, but I think it's acceptable to ignore everything after the #. There is some overhead, but I believe it's minimal.
diff --git i/src/utils/url.ts w/src/utils/url.ts
index 8e4dcb43..a38ea776 100644
--- i/src/utils/url.ts
+++ w/src/utils/url.ts
@@ -113,11 +113,14 @@ export const getPath = (request: Request): string => {
// '%'
// If the path contains percent encoding, use `indexOf()` to find '?' and return the result immediately.
// Although this is a performance disadvantage, it is acceptable since we prefer cases that do not include percent encoding.
- const queryIndex = url.indexOf('?', i)
- const path = url.slice(start, queryIndex === -1 ? undefined : queryIndex)
+ let separator = url.indexOf('?', i)
+ if (separator === -1) {
+ separator = url.indexOf('#', i)
+ }
+ const path = url.slice(start, separator === -1 ? undefined : separator)
return tryDecodeURI(path.includes('%25') ? path.replace(/%25/g, '%2525') : path)
- } else if (charCode === 63) {
- // '?'
+ } else if (charCode === 63 || charCode === 35) {
+ // '?' or '#'
break
}
}
Modifying the default getPath() seems simple, but since it's only used within service workers, I hesitate to change it.
Ignoring the fragment in getPath by default is expected behavior, now. So, it's not an actual bug. I'll remove the triage label and add an enhancement.