URLs can only safely carry a limited set of characters. Anything else — spaces, accented letters, emoji, and characters that have a special
meaning in a URL such as &, ? or # — has to be percent-encoded. Most bugs with URL encoding
come from encoding the wrong part, encoding twice, or confusing two different conventions for spaces.
How percent-encoding works
A character is encoded by taking its UTF-8 bytes and writing each byte as % followed by two hexadecimal digits:
- space →
%20 &→%26é(two bytes in UTF-8) →%C3%A9đ→%C4%91
Letters, digits and - _ . ~ are "unreserved" and never need encoding. The interesting part is the reserved characters —
: / ? # [ ] @ ! $ & ' ( ) * + , ; = — which structure the URL. Whether to encode them depends on whether they are acting as
structure or as data.
Structure vs data: the core rule
In https://example.com/search?q=rock&roll&lang=en the server sees three parameters — q=rock, roll
and lang=en — because the & inside the search term was read as a separator. The search term is data, so its
& must be encoded: q=rock%26roll.
So the rule is: encode each piece of data on its own, then assemble the URL. Never encode a finished URL to "make it safe" — that
either breaks it (encoding the / and ? that are structure) or does nothing useful.
encodeURI vs encodeURIComponent in JavaScript
JavaScript has two functions because there are two jobs:
encodeURI | encodeURIComponent | |
|---|---|---|
| Meant for | A whole URL | One value (a parameter, a path segment) |
| Leaves alone | : / ? # & = + , and other structure | Only unreserved characters |
"a b&c" becomes | a%20b&c | a%20b%26c |
const term = 'rock&roll / 2024';
const url = 'https://example.com/search?q=' + encodeURIComponent(term);
// https://example.com/search?q=rock%26roll%20%2F%202024
Better still, let the platform build query strings for you: new URLSearchParams({ q: term }).toString() or
url.searchParams.set('q', term) encode each value correctly.
The "+" vs "%20" trap
There are two encodings for a space. In the path and in percent-encoding generally, a space is %20. But HTML forms submitted as
application/x-www-form-urlencoded encode spaces as + in the query string. Consequences:
URLSearchParamsand PHP'shttp_build_query()/urlencode()produce+for spaces.encodeURIComponentand PHP'srawurlencode()produce%20.decodeURIComponent('a+b')returns"a+b"— it does not turn+into a space. Decoding form data with it leaves plus signs in the text.- A literal plus sign in data must therefore be sent as
%2B, or a form-decoding server will read it as a space. Phone numbers like+84…are the classic victim.
Double encoding
If %20 appears as %2520, the value was encoded twice (% itself became %25). This happens when a
library already encodes parameters and the code encodes them again before passing them in. Encode exactly once, at the point where the
value enters the URL.
Try it
The URL Encoder & Decoder on Zerethon Tools has both variants described above —
"Component" (encodeURIComponent) for values and "Full URL" (encodeURI) for whole addresses — and runs entirely in your
browser. The URL Parser splits a URL into scheme, hostname, path and decoded
query parameters, which is the quickest way to see whether a URL is being read the way you intended.
Summary
- Encode data, not structure — each value on its own, then assemble.
- Use
encodeURIComponent(orURLSearchParams) for values;encodeURIonly for an entire URL. - Know which space convention your server expects, and send a literal
+as%2B. %25in a URL is the signature of double encoding.