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:

encodeURIencodeURIComponent
Meant forA whole URLOne value (a parameter, a path segment)
Leaves alone: / ? # & = + , and other structureOnly unreserved characters
"a b&c" becomesa%20b&ca%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:

  • URLSearchParams and PHP's http_build_query() / urlencode() produce + for spaces.
  • encodeURIComponent and PHP's rawurlencode() 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 (or URLSearchParams) for values; encodeURI only for an entire URL.
  • Know which space convention your server expects, and send a literal + as %2B.
  • %25 in a URL is the signature of double encoding.