Skip to content

node:url: match URLSearchParams form-encoding set #2978

@andrewtdiz

Description

@andrewtdiz

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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions