Summary
URLSearchParams serialization uses the WHATWG application/x-www-form-urlencoded percent-encode set. Node encodes ~ as %7E and leaves * unescaped. Perry's URLSearchParams encoder currently does the reverse: it treats ~ as unreserved and percent-encodes *.
Node.js behavior
Observed with Node v25.9.0:
const sp = new URLSearchParams();
for (const ch of ["~", "*", "-", ".", "_", " ", "!", "é"]) {
sp.set("x", ch);
console.log(JSON.stringify(ch), "=>", sp.toString());
}
const u = new URL("https://example.com/?x=~&y=%7E");
console.log(u.href, u.searchParams.toString());
u.searchParams.sort();
console.log(u.href);
Node outputs:
"~" => x=%7E
"*" => x=*
"-" => x=-
"." => x=.
"_" => x=_
" " => x=+
"!" => x=%21
"é" => x=%C3%A9
https://example.com/?x=~&y=%7E x=%7E&y=%7E
https://example.com/?x=%7E&y=%7E
Current Perry behavior
crates/perry-runtime/src/url/search_params.rs::url_encode() allows ~ through in the unescaped branch: A-Z, a-z, 0-9, -, _, ., ~.
- The same function percent-encodes every other character, so
* becomes %2A even though Node leaves it literal in URLSearchParams serialization.
maybe_sync_params_to_owner() and js_url_search_params_to_string() both call this encoder, so the mismatch affects both standalone params.toString() and URL search/href updates after searchParams mutations or sort().
Suggested test surface
new URLSearchParams({ x: "~" }).toString() is x=%7E.
new URLSearchParams({ x: "*" }).toString() is x=*.
- Mutating or sorting
new URL("https://example.com/?x=~") .searchParams rewrites href with %7E like Node.
- Existing coverage for spaces as
+ and UTF-8 percent-encoding should remain unchanged.
Scope / non-goals
This is scoped to URLSearchParams form serialization. It is separate from URL path/search setter percent-encode rules and from legacy querystring.escape().
Summary
URLSearchParamsserialization uses the WHATWGapplication/x-www-form-urlencodedpercent-encode set. Node encodes~as%7Eand leaves*unescaped. Perry's URLSearchParams encoder currently does the reverse: it treats~as unreserved and percent-encodes*.Node.js behavior
Observed with Node v25.9.0:
Node outputs:
Current Perry behavior
crates/perry-runtime/src/url/search_params.rs::url_encode()allows~through in the unescaped branch:A-Z,a-z,0-9,-,_,.,~.*becomes%2Aeven though Node leaves it literal in URLSearchParams serialization.maybe_sync_params_to_owner()andjs_url_search_params_to_string()both call this encoder, so the mismatch affects both standaloneparams.toString()and URLsearch/hrefupdates aftersearchParamsmutations orsort().Suggested test surface
new URLSearchParams({ x: "~" }).toString()isx=%7E.new URLSearchParams({ x: "*" }).toString()isx=*.new URL("https://example.com/?x=~") .searchParamsrewriteshrefwith%7Elike Node.+and UTF-8 percent-encoding should remain unchanged.Scope / non-goals
This is scoped to URLSearchParams form serialization. It is separate from URL path/search setter percent-encode rules and from legacy
querystring.escape().