Skip to content

fix(router-core): keep unsafe integer search param literals as strings - #7860

Open
lastMove wants to merge 1 commit into
TanStack:mainfrom
lastMove:fix/unsafe-integer-search-params
Open

fix(router-core): keep unsafe integer search param literals as strings#7860
lastMove wants to merge 1 commit into
TanStack:mainfrom
lastMove:fix/unsafe-integer-search-params

Conversation

@lastMove

@lastMove lastMove commented Jul 20, 2026

Copy link
Copy Markdown

Problem

defaultParseSearch JSON-parses every search param value. Integer literals beyond Number.MAX_SAFE_INTEGER lose precision to float64 rounding:

JSON.parse('120247103250460643') // → 120247103250460640

Any redirect or <Link> that re-serializes the search then writes the corrupted value back into the URL. This silently breaks attribution for marketing landing pages: Meta/TikTok ad and campaign ids are 18–19 digit integers passed as utm_campaign_id/utm_ad_id/etc., so a route that redirects (e.g. /landing/landing/step-1, carrying search) rounds every id before analytics SDKs read location.search. We found this in production after revenue couldn't be matched back to real ad ids — the stored ids all ended in float64-representable digits.

Fix

Integer literals that fail Number.isSafeInteger are kept as strings. The same guard is applied to the parser used by defaultStringifySearch's re-serialization symmetry check, so preserved literals round-trip unquoted (?id=120247103250460643, not ?id=%22120247103250460643%22).

No working behavior is lost: values in this range were already being silently corrupted, so nothing could have depended on the (wrong) numeric result. Safe integers (?page=2, ?ts=1716854400) parse to numbers exactly as before.

Tests

  • New cases: 18-digit positive/negative literals and MAX_SAFE_INTEGER + 2 are preserved as strings and round-trip losslessly; safe integers still parse as numbers.
  • Full router-core suite: 1203 passed, 3 pre-existing expected fails, no type errors.

Changeset included (@tanstack/router-core: patch).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Preserved large integer search parameters as strings when they exceed JavaScript’s safe integer range.
    • Prevented oversized integer values from being silently rounded during parsing, redirects, or link re-serialization.
    • Continued parsing safe integers as numbers.
  • Tests

    • Added coverage for safe and unsafe integer search parameters and lossless round-tripping.

defaultParseSearch JSON-parses every value, so integer literals beyond
Number.MAX_SAFE_INTEGER lose precision to float64 rounding (e.g. the
18-digit Meta ad id 120247103250460643 parses to 120247103250460640).
Any redirect or Link that re-serializes the search then writes the
corrupted value back into the URL — silently breaking ad/click id
attribution for marketing landing pages using the default codec.

Integer literals that fail Number.isSafeInteger now stay strings (both
in defaultParseSearch and the symmetric re-serialization check in
defaultStringifySearch, so they round-trip unquoted). Values that were
previously parsed to numbers in this range were already corrupted, so
no working behavior is lost.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Unsafe integer search parameters

Layer / File(s) Summary
Safe-integer-aware search parsing and validation
packages/router-core/src/searchParams.ts, packages/router-core/tests/searchParams.test.ts, .changeset/big-integer-search-params.md
Search parameter parsing and re-serialization preserve integer literals beyond Number.MAX_SAFE_INTEGER; tests cover unsafe and safe integers, and a patch changeset documents the fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • TanStack/router#7818 — Its loaderDepsHash serialization depends on the router-core stringifySearch behavior updated here.

Suggested labels: package: router-core

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main router-core search param fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/router-core/src/searchParams.ts`:
- Around line 4-18: Update integerLiteralRegex used by
parseJsonPreservingUnsafeIntegers to match JSON integer literals with leading
and trailing whitespace, while preserving the existing optional-negative-sign
and digit constraints. Keep the Number.isSafeInteger(Number(str)) validation and
JSON.parse behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ee64d62-50b5-49f7-8cbd-73a07d905d8c

📥 Commits

Reviewing files that changed from the base of the PR and between edf5575 and 4fa1eaa.

📒 Files selected for processing (3)
  • .changeset/big-integer-search-params.md
  • packages/router-core/src/searchParams.ts
  • packages/router-core/tests/searchParams.test.ts

Comment on lines +4 to +18
const integerLiteralRegex = /^-?\d+$/

/**
* `JSON.parse`, except integer literals beyond `Number.MAX_SAFE_INTEGER` are
* rejected (kept as strings by the callers below): parsing them to a float64
* silently rounds the value (e.g. "120247103250460643" becomes
* 120247103250460640), so any re-serialization corrupts the original — a
* common hazard with 18-digit ad/click ids in marketing URLs.
*/
function parseJsonPreservingUnsafeIntegers(str: string): any {
if (integerLiteralRegex.test(str) && !Number.isSafeInteger(Number(str))) {
throw new Error('Integer literal exceeds safe integer range')
}
return JSON.parse(str)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Account for leading and trailing whitespace in safety checks.

JSON.parse natively ignores leading and trailing whitespace. A value like ?foo=%20120247103250460643%20 will bypass the strict regex check due to the spaces, but will still be successfully parsed and silently rounded by JSON.parse, ultimately corrupting the value on re-serialization.

Updating the regex to allow whitespace ensures these values are properly protected. Number() natively parses whitespace-padded numbers, so the check inside the function will continue to work exactly as expected.

🛡️ Proposed fix to handle whitespace
-const integerLiteralRegex = /^-?\d+$/
+const integerLiteralRegex = /^\s*-?\d+\s*$/
 
 /**
  * `JSON.parse`, except integer literals beyond `Number.MAX_SAFE_INTEGER` are
  * rejected (kept as strings by the callers below): parsing them to a float64
  * silently rounds the value (e.g. "120247103250460643" becomes
  * 120247103250460640), so any re-serialization corrupts the original — a
  * common hazard with 18-digit ad/click ids in marketing URLs.
  */
 function parseJsonPreservingUnsafeIntegers(str: string): any {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const integerLiteralRegex = /^-?\d+$/
/**
* `JSON.parse`, except integer literals beyond `Number.MAX_SAFE_INTEGER` are
* rejected (kept as strings by the callers below): parsing them to a float64
* silently rounds the value (e.g. "120247103250460643" becomes
* 120247103250460640), so any re-serialization corrupts the original a
* common hazard with 18-digit ad/click ids in marketing URLs.
*/
function parseJsonPreservingUnsafeIntegers(str: string): any {
if (integerLiteralRegex.test(str) && !Number.isSafeInteger(Number(str))) {
throw new Error('Integer literal exceeds safe integer range')
}
return JSON.parse(str)
}
const integerLiteralRegex = /^\s*-?\d+\s*$/
/**
* `JSON.parse`, except integer literals beyond `Number.MAX_SAFE_INTEGER` are
* rejected (kept as strings by the callers below): parsing them to a float64
* silently rounds the value (e.g. "120247103250460643" becomes
* 120247103250460640), so any re-serialization corrupts the original a
* common hazard with 18-digit ad/click ids in marketing URLs.
*/
function parseJsonPreservingUnsafeIntegers(str: string): any {
if (integerLiteralRegex.test(str) && !Number.isSafeInteger(Number(str))) {
throw new Error('Integer literal exceeds safe integer range')
}
return JSON.parse(str)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/searchParams.ts` around lines 4 - 18, Update
integerLiteralRegex used by parseJsonPreservingUnsafeIntegers to match JSON
integer literals with leading and trailing whitespace, while preserving the
existing optional-negative-sign and digit constraints. Keep the
Number.isSafeInteger(Number(str)) validation and JSON.parse behavior unchanged.

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.

1 participant