defaultParseSearch corrupts string search params that look like JSON numbers (e.g. "662E41", large integers) — lossy and unrecoverable on inbound URLs
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Router
Describe the bug
defaultParseSearch runs JSON.parse on every search-param value and keeps the
result whenever it parses to a number. This destructively coerces values that are
opaque strings, not numbers — authorization codes, signatures/HMACs, hex IDs,
all-digit ULIDs, etc. — as long as they match the JSON number grammar.
Two consequences:
- Lossy / irreversible.
662E41(a valid 6-char hex/auth code) →6.62e+43;
723421968459640832→723421968459640800. The original string cannot be
reconstructed. - Unrecoverable for inbound/external URLs. When the value arrives from an
external redirect (OAuth callback, payment-gateway return, …), the coercion
happens insideparseSearchbeforevalidateSearchruns. By the time a Zod
schema sees it, it is already a lossy number —z.coerce.string()/z.string()
cannot recover the original, they only see6.62e+43.
This is the data-corruption root cause underneath the quote-wrapping symptom in
#6044 and the string/number typing confusion in #537 / discussion #430, but the
failure mode here is destruction of non-numeric data, not formatting.
Complete minimal reproducer
https://stackblitz.com/edit/github-hjjlagts?file=src%2Froutes%2Findex.tsx
Steps to Reproduce the Bug
import { defaultParseSearch } from '@tanstack/router-core' // re-exported by @tanstack/react-router
defaultParseSearch('?codAut=662E41')
// actual: { codAut: 6.62e+43 }
// expected: { codAut: "662E41" }
defaultParseSearch('?id=723421968459640832')
// actual: { id: 723421968459640800 } // precision loss
// expected: { id: "723421968459640832" }
A route with validateSearch: z.object({ codAut: z.string() }) cannot guard an
inbound value: it's already a number before Zod runs.
Expected behavior
A param value that does not survive a numeric round-trip should be kept as the
original string. Canonical numbers / booleans / objects should still be coerced.
Screenshots or Videos
No response
Platform
- Router / Start Version: @tanstack/react-router 1.168.25 (router-core 1.168.17) · @tanstack/react-start 1.167.50
- OS: macOS 26.5 (Darwin 25.5.0)
- Browser: Chrome
- Browser Version: 149.0.0
- Bundler: vite
- Bundler Version: 7.3.2
Additional context
Root cause
qss's toValue already guards against lossy coercion:
// qss.ts — toValue
return +str * 0 === 0 && +str + "" === str ? +str : str; // keep string if it doesn't round-trip
…but parseSearchWith(JSON.parse) re-applies JSON.parse to the strings toValue
deliberately left alone, defeating that guard:
// searchParams.ts — parseSearchWith
const query = decode(searchStr); // toValue kept "662E41" as a string
for (const key in query) {
if (typeof query[key] === 'string')
query[key] = JSON.parse(query[key]); // => 6.62e+43 (re-introduces precision loss)
}
Proposed fix
Apply the same round-trip guard in the default parser — accept JSON.parse's
result only when it isn't a number, or when String(result) === input; otherwise
keep the original string:
const parsed = JSON.parse(value)
return typeof parsed === 'number' && String(parsed) !== value ? value : parsed
Preserves coercion for "2", "true", "0.5", {"a":1}, …, while keeping
662E41, large integers, leading-zero strings and other non-round-tripping values
intact. Happy to open a PR if the team agrees with the direction.
Real-world impact
A payment-gateway return URL echoes an auth code (codAut) the backend uses to
recompute an HMAC. Coercing 662E41 → "6.62e+43" corrupts the field, the
signature no longer matches, and the capture is rejected (HTTP 403) even though the
payment succeeded.
Related
#6044 — quote-wrapping (downstream symptom of the same coercion)
#537 — string/number coercion typing
Discussion #430
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in searchParams.ts at parseSearchWith and compare its JSON.parse handling with the round-trip guard in qss.ts. Reproduce the defaultParseSearch examples for 662E41 and 723421968459640832, then add regression coverage showing canonical values still coerce while lossy numeric-looking strings remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100