Skip to content

fix(url): ignore fragment identifiers in getPath()#4627

Merged
yusukebe merged 5 commits intohonojs:mainfrom
sano-suguru:fix/getpath-ignore-fragment
Feb 8, 2026
Merged

fix(url): ignore fragment identifiers in getPath()#4627
yusukebe merged 5 commits intohonojs:mainfrom
sano-suguru:fix/getpath-ignore-fragment

Conversation

@sano-suguru
Copy link
Contributor

Summary

  • Modified getPath() to strip fragment identifiers (#) from URLs
  • This fixes incorrect route parameter parsing in Service Worker contexts

Fixes #4440

Changes

In Service Worker contexts, URLs may contain fragment identifiers (e.g., /users/1#profile-section) which were incorrectly included in route parameters. This change modifies getPath() to strip fragment identifiers, consistent with how query strings are already handled.

Before

/users/#user-list → id = "#user-list"
/users/1#profile-section → id = "1#profile-section"

After

/users/#user-list → matches /users/
/users/1#profile-section → id = "1"

Implementation

Based on the approach suggested by @usualoma in #4440:

  • Added # (charCode 35) to the loop termination condition
  • Handle fragment identifiers in the percent-encoded path branch

Test plan

  • Added tests for URLs with fragments
  • Added tests for URLs with both query strings and fragments
  • Added tests for percent-encoded paths with fragments
  • All existing tests pass

sanosuguru added 2 commits January 14, 2026 02:18
In Service Worker contexts, URLs may contain fragment identifiers (#)
which should not be included in route matching. This change modifies
getPath() to strip fragment identifiers from the path, consistent with
how query strings are already handled.

Fixes honojs#4440
When a URL contains a fragment with query-like characters (e.g., #section?foo=bar),
the previous logic would incorrectly include the fragment in the path because it
checked for '?' first and only looked for '#' if '?' was not found.

This fix uses Math.min() to find whichever delimiter ('?' or '#') comes first,
correctly handling edge cases per RFC 3986 where '#' terminates the path regardless
of subsequent '?' characters in the fragment.

Added test case for this edge case.
@sano-suguru
Copy link
Contributor Author

Fixed: Percent-encoded path with fragment containing query-like characters

The previous implementation had a bug where it would check for ? first, and only look for # if ? was not found:

let separator = url.indexOf('?', i)
if (separator === -1) {
  separator = url.indexOf('#', i)
}

This caused incorrect behavior when a URL contains a fragment with query-like characters (e.g., https://example.com/hello%20world#section?foo=bar). In such cases, per RFC 3986, the ? after # is part of the fragment, not a query delimiter.

Fix

Changed to find whichever delimiter (? or #) comes first using Math.min():

const queryIndex = url.indexOf('?', i)
const hashIndex = url.indexOf('#', i)
const separator =
  queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex)

Added Test

it('getPath - with percent encoding and fragment containing query-like chars', () => {
  const path = getPath(new Request('https://example.com/hello%20world#section?foo=bar'))
  expect(path).toBe('/hello world')
})

All tests pass.

Verify that percent-encoded hash (%23) is preserved while real fragment
identifiers are correctly stripped. decodeURI preserves reserved characters
per Web Standards.
Copy link
Member

@usualoma usualoma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for creating the pull request! I think the direction is good.

With the current code, === -1 is evaluated three times for /path?x=1, so I think the following is better:

      const end =
        queryIndex === -1
          ? hashIndex === -1
            ? undefined
            : hashIndex
          : hashIndex === -1
          ? queryIndex
          : Math.min(queryIndex, hashIndex)
      const path = url.slice(start, end)

@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.44%. Comparing base (28452f0) to head (8bdd859).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
src/utils/url.ts 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4627      +/-   ##
==========================================
- Coverage   91.44%   91.44%   -0.01%     
==========================================
  Files         173      173              
  Lines       11321    11330       +9     
  Branches     3280     3286       +6     
==========================================
+ Hits        10353    10361       +8     
- Misses        967      968       +1     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

sanosuguru and others added 2 commits January 14, 2026 11:13
Restructure ternary to avoid redundant separator === -1 check.
For /path?x=1, reduces evaluations from 3 to 2.

Co-authored-by: usualoma <usualoma@users.noreply.github.com>
Cover the undefined branch when both queryIndex and hashIndex are -1.
Copy link
Member

@usualoma usualoma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@sano-suguru
Copy link
Contributor Author

Thank you for the review and the optimization suggestion! I've updated the code as you suggested.

The restructured ternary now avoids the redundant separator === -1 check, reducing evaluations from 3 to 2 for URLs like /path?x=1.

I also added a test case to cover the undefined branch (when both queryIndex and hashIndex are -1).

(Sorry if my English is a bit rough - it's not my native language!)

const queryIndex = url.indexOf('?', i)
const path = url.slice(start, queryIndex === -1 ? undefined : queryIndex)
const hashIndex = url.indexOf('#', i)
const end =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about the following implementation? This is more short and readable.

const end = Math.min(
  queryIndex === -1 ? url.length : queryIndex,
  hashIndex === -1 ? url.length : hashIndex
)

This runs Math.min every time, but in some cases that contain percent encoding, the path has a query.

@sano-suguru @usualoma What do you think of it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yusukebe
Sorry I missed that. Either one seems fine.
The character count after minification is probably the same, right?

Math.min(a===-1?u.length:a,b===-1?u.length:b)
a===-1?b===-1?void 0:b:b===-1?a:Math.min(a,b)

Performance-wise, there probably won't be a noticeable difference in production. I prefer the current implementation, but either one is fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed that. Either one seems fine.

No problem!

The character count after minification is probably the same, right?

Ah, yes. So it's just for readability. If you feel both are okay, I think we should keep the current implementation.

Copy link
Member

@yusukebe yusukebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yusukebe
Copy link
Member

yusukebe commented Feb 8, 2026

@sano-suguru

I've left a small comment, but let's go with this implementation. Looks good. Thanks!

@yusukebe yusukebe merged commit 0c1d4c7 into honojs:main Feb 8, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fragment identifiers cause incorrect route parameter parsing in Service Worker

3 participants