URL encoding diagnostics · Updated November 3, 2026

URL Encode Query Reserialization: Same Values, Different URL Bytes

URLSearchParams.sort() can change the serialized query even when every decoded key and value stays the same. For example, ?q=a%20b~ becomes ?q=a+b%7E. The spaces, tilde and decoded pair are unchanged; the URL bytes are not.

This browser-only preview does not send a request. It shows the query before and after mutating the URL object's searchParams.

What changed, and what did not

  1. Keep the original url.search string before touching searchParams.
  2. Read the decoded pairs, call url.searchParams.sort(), then read the pairs again.
  3. Compare both the pairs and the serialized query. Equal pairs do not prove equal URL bytes.
  4. If a signature, cache key or audit record uses the original spelling, treat reserialization as a meaningful mutation and test it at that boundary.

Ten fixtures run on Node v24.19.0

The table records a real local run. It is a diagnostic of the WHATWG URL and URLSearchParams implementation, not a promise about every framework's signing or caching policy.

Input queryAfter sort()Decoded pairs preserved?Byte spelling changed?
?q=a%20b~?q=a+b%7EYesYes
?x=1&x=2?x=1&x=2YesNo
?empty=?empty=YesNo
?flag?flag=YesYes
?z=%2F&a=1?a=1&z=%2FYesYes
?space=%20?space=+YesYes
?tilde=~?tilde=%7EYesYes
?mixed=a%2Bb+c?mixed=a%2Bb+cYesNo
?name=John+Doe?name=John+DoeYesNo
?b=2&a=1?a=1&b=2YesYes

Why equal values can still break a boundary

The decoded pair ["q", "a b~"] is stable, but %20 becomes + and ~ becomes %7E. A system that signs the exact query string, compares cache keys byte-for-byte, or stores a canonical URL can therefore observe a change after a harmless-looking sort. That is an observation from this runtime, not a universal policy recommendation.

Sorting is stable for duplicate names: ?x=1&x=2 keeps both values and their order in this run. That differs from converting entries to an object, which is a separate application-level choice and is not performed by this page.

Download and rerun

Download the Node regression script, ten fixtures and recorded observations. Run:

node downloads/query-reserialization-regression.cjs

Expected output is a passing fixture count and your local Node version. The script asserts decoded-pair preservation and the spelling changes shown above.

For ordinary component escaping, return to the URL encoder. For repeated-key behavior, see the repeated query parameter checker; this page isolates reserialization after sort().