diff --git a/.changeset/parsesearch-numeric-roundtrip.md b/.changeset/parsesearch-numeric-roundtrip.md new file mode 100644 index 0000000000..de34007be5 --- /dev/null +++ b/.changeset/parsesearch-numeric-roundtrip.md @@ -0,0 +1,9 @@ +--- +'@tanstack/router-core': patch +--- + +fix(router-core): preserve string search params whose value is a valid JSON number but does not round-trip (e.g. `662E41`, large integers beyond safe range) + +`parseSearchWith(JSON.parse)` called `JSON.parse` on every string query-param value and kept the result whenever it parsed successfully — including scientific-notation forms like `662E41` (= 6.62e43) and integers beyond `Number.MAX_SAFE_INTEGER`. Because `String(6.62e+43) !== '662E41'`, the original string was irreversibly destroyed before `validateSearch` could run. + +The fix adds a numeric round-trip guard: if `JSON.parse` returns a `number` and `String(number) !== originalString`, the original string is kept instead. diff --git a/packages/router-core/src/searchParams.ts b/packages/router-core/src/searchParams.ts index 740d36441c..d466dca0b8 100644 --- a/packages/router-core/src/searchParams.ts +++ b/packages/router-core/src/searchParams.ts @@ -32,7 +32,15 @@ export function parseSearchWith(parser: (str: string) => any) { const value = query[key] if (typeof value === 'string') { try { - query[key] = parser(value) + const parsed = parser(value) + // Reject numeric parses that do not round-trip back to the original + // string (e.g. '662E41' → 6.62e+43, '723421968459640832' → precision + // loss). Keep the original string so callers receive what was in the URL. + if (typeof parsed === 'number' && String(parsed) !== value) { + // silent — keep original string + } else { + query[key] = parsed + } } catch (_err) { // silent } diff --git a/packages/router-core/tests/searchParams.test.ts b/packages/router-core/tests/searchParams.test.ts index 006e119be5..459b129e77 100644 --- a/packages/router-core/tests/searchParams.test.ts +++ b/packages/router-core/tests/searchParams.test.ts @@ -98,4 +98,17 @@ describe('Search Params serialization and deserialization', () => { '?foo=%222024-11-18T00%3A00%3A00.000Z%22', ) }) + + test('numeric round-trip: strings that look like JSON numbers but do not survive round-trip should stay as strings', () => { + // '662E41' is valid JSON scientific notation (6.62e43), but String(6.62e43) !== '662E41' + expect(defaultParseSearch('?codAut=662E41')).toEqual({ codAut: '662E41' }) + // Large integer beyond safe range — precision would be lost + expect(defaultParseSearch('?id=723421968459640832')).toEqual({ + id: '723421968459640832', + }) + // '9e3' parses as 9000 but String(9000) !== '9e3' + expect(defaultParseSearch('?n=9e3')).toEqual({ n: '9e3' }) + // Regular integers that do round-trip cleanly should still parse as numbers + expect(defaultParseSearch('?count=42')).toEqual({ count: 42 }) + }) })