Skip to content

fix(client): keep a param value of "index" in $url() and $path() - #5297

Merged
yusukebe merged 1 commit into
honojs:mainfrom
MahathirMohammadShuvo:fix/client-url-index-param
Aug 27, 2026
Merged

yusukebe merged 1 commit into
honojs:mainfrom
MahathirMohammadShuvo:fix/client-url-index-param

Conversation

@MahathirMohammadShuvo

Copy link
Copy Markdown
Contributor

Summary

removeIndexString() strips a trailing /index so the synthetic .index
accessor resolves to the route it aliases. In the $url() / $path() branch it
runs after the path params have been substituted, so anything that merely
looks like the alias once substitution is done gets stripped as well.

Moving the call ahead of substitution fixes it: the alias is part of the route
template, and a param value is not.

Before / after

Typed route Call Before After
/docs/:page param: { page: 'index' } /docs /docs/index
/:page param: { page: 'index' } / /index
/index/:v? param: { v: undefined } / /index

The third row is a route with a literal /index segment rather than a param
value. Dispatching against that route alone, / returns 404 and /index
returns 200, so the generated URL did not point at the route it came from.

Why this ordering

ClientRequestImpl.fetch has done removeIndexString → replaceUrlParam since
hc was introduced in #862, which is why $get() already returns the right path
for all three rows above and only $url() / $path() disagreed.

This is a follow-up to #4267, which added the removeIndexString call to this
branch (fixing #4265) and placed it at the end of the sequence, immediately
before return new URL(result). Moving it to the front brings the branch in line
with fetch.

removeIndexString's (?=\?|$) lookahead, added in #4352, is left as-is: it is
still exercised directly by src/client/utils.test.ts, even though no hc call
site now passes it a query string.

Relationship to #5291

#5291 adds the same normalization to the $ws() branch. The two are independent
hunks in the same function and complementary. I merged them locally in a scratch
branch to check: no conflict, and src/client/client.test.ts passes 128 with
both applied. With both in, all four accessors ($get(), $url(), $path(),
$ws()) strip the alias from the route template before substituting params.
Happy to rebase on top of #5291 if it lands first.

Tests

5 tests added, 9 executions — four of them sit in the $path / $url
describe.each, and one in the query-option block. 7 fail on main. The one
that passes either way — Should still drop the index alias of a route with a param — is a regression guard that the genuine alias is still removed.

Verification

  • npx vitest --run src/client/client.test.ts — 127 passed (120 with client.ts
    reverted to main, 7 failed)
  • npx tsc -p tsconfig.spec.json — exit 0
  • npx eslint src — exit 0
  • Prettier clean on the committed blobs
  • Full npx vitest --run: green with --coverage.enabled=false. With coverage
    on I see at least one pre-existing 5 s timeout,
    src/middleware/compress/index.test.ts "should compress streaming responses
    written in multiple chunks", which reproduces identically on a clean main
    checkout and passes 45/45 in isolation.

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code (verified with prettier --check and eslint directly; the fixer itself rewrites every line ending on a CRLF checkout)
  • Add TSDoc/JSDoc to document the code

removeIndexString() strips a trailing /index so that the synthetic .index
accessor resolves to the route it aliases. In the $url()/$path() branch it ran
after the params had been substituted, so anything that merely looked like the
alias once substitution was done got stripped as well:

  client.docs[':page'].$path({ param: { page: 'index' } })   // "/docs"
  client.index[':v?'].$path({ param: { v: undefined } })     // "/"

The second is a route with a literal /index segment; "/" does not match it and
returns 404, so that URL never pointed at the route it came from.

The alias belongs to the route template and neither of those does, so the strip
now runs before substitution. ClientRequestImpl.fetch already does it in that
order, which is why $get() has always returned the right path for these routes
while $url() and $path() did not.
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.87%. Comparing base (017000d) to head (1680bdb).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5297      +/-   ##
==========================================
- Coverage   79.88%   79.87%   -0.01%     
==========================================
  Files         156      156              
  Lines       10925    10924       -1     
  Branches     2301     2301              
==========================================
- Hits         8727     8726       -1     
  Misses       2198     2198              

☔ View full report in Codecov by Harness.
📢 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.

@yusukebe yusukebe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

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.

2 participants