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
- Keep the original
url.searchstring before touchingsearchParams. - Read the decoded pairs, call
url.searchParams.sort(), then read the pairs again. - Compare both the pairs and the serialized query. Equal pairs do not prove equal URL bytes.
- 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 query | After sort() | Decoded pairs preserved? | Byte spelling changed? |
|---|---|---|---|
?q=a%20b~ | ?q=a+b%7E | Yes | Yes |
?x=1&x=2 | ?x=1&x=2 | Yes | No |
?empty= | ?empty= | Yes | No |
?flag | ?flag= | Yes | Yes |
?z=%2F&a=1 | ?a=1&z=%2F | Yes | Yes |
?space=%20 | ?space=+ | Yes | Yes |
?tilde=~ | ?tilde=%7E | Yes | Yes |
?mixed=a%2Bb+c | ?mixed=a%2Bb+c | Yes | No |
?name=John+Doe | ?name=John+Doe | Yes | No |
?b=2&a=1 | ?a=1&b=2 | Yes | Yes |
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().