hono icon indicating copy to clipboard operation
hono copied to clipboard

Fragment identifiers cause incorrect route parameter parsing in Service Worker

Open yatsuna827 opened this issue 2 months ago • 4 comments

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-list matches /users/ route
  • /users/1#profile-section matches /users/:id route with id = "1"

What do you see instead?

  • /users/#user-list matches /users/:id route with id = "#user-list"
  • /users/1#profile-section matches /users/:id but with id = "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

yatsuna827 avatar Oct 04 '25 13:10 yatsuna827

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'.

yatsuna827 avatar Oct 04 '25 14:10 yatsuna827

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.

usualoma avatar Oct 05 '25 00:10 usualoma

I think the following options are available.

  • Do nothing
    • Keep it as is, letting users specify it via the new Hono() options
  • 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.

usualoma avatar Oct 05 '25 06:10 usualoma

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.

yusukebe avatar Oct 07 '25 01:10 yusukebe