Skip to content
Allin

Building a URL With Parameters That Survives a Copy-Paste

Published 8/11/2026 · 12 min read · Developer tools

Daniel Okonkwo

Daniel OkonkwoFront-end developer and tech writer at Allin

Web performance · File formats

Checked against 4 sources

View profile
In short

A query string breaks when a character that means something to the URL — a space, an ampersand, an equals sign, a hash — is left as itself inside a value. Percent-encoding fixes that, but there are three encodings and they disagree, and the disagreement is visible on the space. encodeURIComponent writes a space as %20 and leaves ! ' ( ) * ~ alone. The application/x-www-form-urlencoded serializer, which is what URLSearchParams and every HTML form use, writes a space as + and percent-encodes ! ' ( ) ~ while leaving * alone. Strict RFC 3986 percent-encodes everything outside A-Z a-z 0-9 - . _ ~, so it escapes * as well and keeps ~. This builder offers all three and its form mode was checked against Node's own URLSearchParams on seventeen values, including an emoji, a newline and an accented string: byte for byte identical every time. It does double-encode: type %20 as a value and you get %2520. That is correct — you are typing a raw value, not a pre-encoded one — and the tool's own parser round-trips it back to the literal %20. The real bug is elsewhere. Give it a base URL containing a fragment, https://example.com/page#section, and the query is appended after the fragment: the browser's own URL parser then reports an empty search and puts every parameter inside the hash, where no server will ever see it. Its parser also throws the fragment away, and a legacy escape such as %E9 fails to decode, is kept as literal text and comes back as %25E9 on the next build.

Three encodings, one visible difference: %20 or +. The builder's form mode matches URLSearchParams byte for byte on seventeen values — but give it a base URL with a fragment and every parameter lands inside the hash, where no server sees it.

Two standards, and browsers implement the newer one

RFC 3986 is the older document and the one everyone cites. It divides characters into unreserved — A-Z, a-z, 0-9 and the four marks - . _ ~ — and everything else, which is reserved or has to be percent-encoded. It is a clean model and it is not what your browser runs. Browsers implement the WHATWG URL Standard, a living document that specifies parsing and serialising in terms of several named percent-encode sets, one per part of the URL. The two agree on the common cases and diverge on the edges, and the edges are where a URL stops working.

The URL Standard is explicit about the relationship. It says that using its component percent-encode set with UTF-8 gives identical results to JavaScript's encodeURIComponent. And it defines the application/x-www-form-urlencoded percent-encode set as the component set plus ! ' ( ) ~ — then states the same thing the other way round, in the sentence that is worth memorising: that set contains all code points except the ASCII alphanumerics and * - . _ . Four characters. That is the entire safe list of a form submission, and it explains at a glance why the two modes disagree exactly on ! ' ( ) ~ while both leave the asterisk alone.

The builder's third mode, strict RFC 3986, is the one to reach for when a server signs the URL. It takes encodeURIComponent's output and additionally escapes ! ' ( ) * , leaving ~ alone, because the tilde is unreserved in RFC 3986 and escaping it would change the signature. Payment gateways, OAuth 1.0 signatures and some older enterprise APIs compute a hash over the encoded string, so an encoder that leaves an apostrophe as itself produces a different hash from theirs and the request is rejected with no explanation you can act on.

Yes it double-encodes, and that is the right answer

Double-encoding is the classic complaint about URL builders, so it was tested directly. Type %20 into a value field and the output is %2520. Type caf%C3%A9 and you get caf%25C3%25A9. Type a single percent sign and you get %25. That is not a bug; it is the definition of the field. The value box holds the value your user typed, and if that value genuinely is the five characters c a f % 2 0, then %2520 is the only encoding that will deliver them.

The way to check whether a builder has this right is not to look at one output but to run a round trip. Build a URL, paste it back into the tool's own parse field, and build it again. Six values were put through that cycle — an accented phrase with an ampersand, a plus sign, a literal %20, a hash inside a value, an empty value and a JSON object — and all six came back byte-identical. The pasted %2520 decoded to the literal %20, which re-encoded to %2520. A builder that stripped one layer to look tidy would fail exactly here, silently, on the one value where it matters.

The bug: a fragment in the base URL swallows every parameter

The builder joins its query to the base by looking for a question mark: if the base already has one it appends with an ampersand, otherwise it appends with a question mark. Both branches append at the end. A URL does not work that way. The order of the parts is fixed — scheme, authority, path, query, fragment — and the fragment is last, so anything written after it belongs to it.

Set the base to https://example.com/page#section and add q and page, and the tool prints https://example.com/page#section?q=caf%C3%A9%20%26%20croissant&page=2. It looks fine. Hand that string to the browser's own URL parser and it reports a pathname of /page, a search of the empty string, and a hash containing #section?q=caf%C3%A9%20%26%20croissant&page=2 — every parameter inside the fragment. A fragment is never sent to the server. The page will load, no error will appear anywhere, and the parameters will simply not exist as far as anything on the server side is concerned. The variant with a query already present, https://example.com/page?a=1#frag, is the same story: search comes back as ?a=1 and the new parameters are inside the hash.

The parse direction has the mirror problem: it splits the input at the first hash and throws away everything after it. Paste https://example.com/p?a=1#frag and the base comes back as https://example.com/p, fragment gone. So you cannot use the tool at all on a URL that has a fragment — it loses it on the way in and misplaces the query on the way out. Until that is fixed, the workaround is to strip the fragment yourself, build the query, and reassemble by hand: base, then the query, then the hash, in that order.

Three smaller things worth knowing before you rely on it

First, a percent-escape it cannot decode is kept as literal text and then re-encoded on the way out. Paste a URL containing a=%E9 — a single-byte Latin-1 escape, which is what a great many pre-2010 systems still emit — and the decoder throws, the tool catches the error and keeps the three characters % E 9 as the value. Build again and it becomes a=%25E9, a different URL from the one you pasted. Nothing warns you. The safe move is to check any parameter that comes back containing a percent sign.

Second, the builder groups parameters by key rather than keeping your row order. Rows a=1, b=2, a=3 come out as a=1&a=3&b=2: the two a rows are pulled together and b moves down. For a normal web server this is harmless, since almost nothing cares about parameter order. For a signed request it is not, because the signature is computed over the exact string. Check the output against the order you typed whenever the receiving end hashes the query.

Third, the sort keys option sorts with the browser's locale-aware comparison, not by code point. Given the keys b, a, B and _x it produces _x, a, b, B. A code-point sort — the one every signing scheme in existence specifies — produces B, _x, a, b. The two differ whenever your keys mix upper and lower case or start with an underscore, which is exactly what a signed API's parameter names tend to do. Use the option to make a long URL readable; do not use it to canonicalise one.

Two things it gets right that are easy to get wrong. Its four repeated-value syntaxes all percent-encode the brackets, so a[]=1 is written a%5B%5D=1 and a[0]=1 as a%5B0%5D=1, which is correct under RFC 3986 where square brackets are reserved for IPv6 host literals — and which PHP, Rails and Express all decode. And in comma mode the comma between values is encoded too, so color=red,blue is written color=red%2Cblue: legal, and safe with any server that decodes before it splits.

The same value through the builder's three encodings, as it actually printed them
Value typedencodeURIComponentx-www-form-urlencodedStrict RFC 3986
two wordstwo%20wordstwo+wordstwo%20words
café & croissantcaf%C3%A9%20%26%20croissantcaf%C3%A9+%26+croissantcaf%C3%A9%20%26%20croissant
C'est (chouette) !C'est%20(chouette)%20!C%27est+%28chouette%29+%21C%27est%20%28chouette%29%20%21
~tilde*star~tilde*star%7Etilde*star~tilde%2Astar
a=b&c#da%3Db%26c%23da%3Db%26c%23da%3Db%26c%23d
%20 typed literally%2520%2520%2520
Query String BuilderBuild a URL query string from key/value rows with correct percent-encoding, array syntax options and a UTM preset — or parse an existing URL back into rows.Try the tool

Frequently asked questions

Which encoding should I pick if I do not know what the server expects?
Pick encodeURIComponent, the default. A space becomes %20, which every server decodes correctly, whereas + is only decoded as a space by code that knows it is reading form data. That asymmetry is the whole reason to prefer %20 in a URL you are going to paste into an email, a chat message or a spreadsheet: it survives being read by something that does not know it came from a form. Switch to x-www-form-urlencoded only when you are reproducing what a browser form would send — for example when you are debugging why a form submission differs from your hand-built URL. Switch to strict RFC 3986 when a server signs or hashes the query string, because the extra escapes for ! ' ( ) * are what most signing libraries produce.
Why did my parameters disappear when the link had a #section on the end?
Because they were written after the fragment, and everything after a hash is the fragment. This builder appends the query at the end of whatever you gave it as a base, so a base of https://example.com/page#section produces https://example.com/page#section?q=x, and the browser reads the whole tail as one fragment. A fragment is resolved entirely on the client; it is not part of the request line and no server ever sees it. Fix it by hand: take the base without its fragment, add the query, then add the fragment back at the very end, giving https://example.com/page?q=x#section. If you paste that corrected URL back into this tool it will parse the query correctly but drop the fragment again, so keep the fragment somewhere else while you work.
How do I send a list of values for the same parameter?
There is no standard, which is why the tool offers four syntaxes. Repeating the key, color=red&color=blue, is what an HTML form produces when several checkboxes share a name, and it is what URLSearchParams.getAll returns; it is the safest default. Brackets, color[]=red&color[]=blue, are a PHP convention that Rails and several PHP frameworks also read. Indexed brackets, color[0]=red, preserve position and are used where the server rebuilds an ordered array. A comma-joined value, color=red,blue, is one parameter with one value that the server splits itself. Pick whichever your server documents; if it documents none, repeat the key. Note that the tool percent-encodes the brackets and the comma, which is legal and which every one of those frameworks decodes before parsing.
Is it safe to put an email address or an order number in a query string?
Technically it will work, and you should still avoid it. A query string is part of the URL, and URLs are written to the browser's history, to server access logs, to proxy and CDN logs, and to the Referer header sent to whatever third-party script the page loads. Any personal data you put there is copied into all of those places by systems that were never told it was personal, and it stays there for whatever retention period each of them has. Put identifiers in the query string by all means — a product id, a page number, a campaign name — and put anything that identifies a person in the request body of a POST, or behind a short opaque token that your own server resolves. The same applies to anything that grants access: a token in a URL is a token in a log file.
How long can a URL be before something truncates it?
No specification sets a limit; every implementation sets its own, and the one that bites first is usually not the browser. RFC 3986 explicitly declines to impose a maximum and instead advises that anything handling URLs should cope with lengths beyond what it expects. In practice, browsers accept tens of thousands of characters, while web servers and proxies commonly refuse a request line above about 8 kilobytes and answer with a 414 status. Search-result and analytics tooling truncates far earlier than that. The practical advice is unchanged by any exact figure: if your query string is running into thousands of characters, the data belongs in a POST body or behind a short identifier, not in a link. Long URLs also break when pasted into email clients and chat apps, which wrap them at a column and turn one link into two.

Articles you may find interesting

All guides
ExplainerWhat Is a QR Code?A QR code is a 2D barcode a camera reads to open a link or text. Here's what it is, why it holds so much, its anatomy, and a safety note on scanning.ExplainerWhat Is a Cron Expression?A cron expression schedules a task to run automatically at set times. Here's what it's for, its five fields, how to read one, and the common gotchas.GuideURL Encoding Explained: Percent-Encoding and Where It BitesPercent-encoding is decided per URL component, which is the whole source of the confusion. A slash is legal in a path and must be escaped in a query value; a space is %20 in a path and may be + in a form body. Here are the exact RFC 3986 sets, the three JavaScript functions that disagree, and the traps.ExplainerExtracting Every Email Address or URL from a Block of TextA URL at the end of a sentence keeps the full stop; an email address at the end of the same sentence does not. An accented first name in an address comes back truncated. Every case here was run through the tools and the exact output is reported.ExplainerWhat Is Inside a JWT — and What It Does Not ProtectA JWT is signed, not encrypted. Anyone holding the token can decode the payload and read every claim in it. Here is a real token, decoded without any key, plus the three attacks the signature is supposed to stop and the one problem it cannot solve.ComparisonJSON vs XML: What's the Difference?JSON and XML both store structured data as text, but they trade off differently. Here's how each looks, where each wins, and how to choose.

Related tools

These figures come from running these tools on real files and compressing the result, not from a vendor's claim. Byte counts depend entirely on the file: a stylesheet written with long comments compresses differently from one written without them, and your numbers will not be ours. Minifiers are also not all equivalent — two on this site disagree about the same input — so treat any minified output as new code that has to be looked at before it ships. Keep the readable original in version control, minify at build time, and check the page in a browser before you publish.

Sources

Spotted a mistake in this article?