Why does a space sometimes become %20 and sometimes +? Why does a link break when a search term contains &? And why do you occasionally see %2520 in an address bar? All three come down to URL encoding, also called percent-encoding, and to the fact that there are two slightly different rule sets for it.
This guide covers the rules from the URL standard, the difference between %20 and +, which JavaScript function to use when, and the bugs that come from getting it wrong.
Why URLs need encoding
A URL can only contain a limited set of characters, and some of those characters have jobs. / separates path segments, ? starts the query string, & separates parameters, = separates a name from its value, and # starts the fragment. If your data contains one of these characters, it has to be written in a way that can't be mistaken for structure.
RFC 3986, the URI standard, splits characters into groups:
- Unreserved: letters, digits and
- . _ ~. These never need encoding. - Reserved:
: / ? # [ ] @and! $ & ' ( ) * + , ; =. These have special meaning and must be encoded when used as data. - Everything else, including spaces, quotes,
%and all non-ASCII characters, always has to be encoded.
How percent-encoding works
Each byte that needs encoding is written as % followed by two hexadecimal digits. A space is byte 32, or 20 in hex, so it becomes %20. An ampersand becomes %26, and a percent sign itself becomes %25.
Non-ASCII characters are first converted to UTF-8 bytes, then each byte is encoded, as the RFC specifies for new URI schemes. That's why one character can turn into several codes:
é → %C3%A9
€ → %E2%82%AC
👍 → %F0%9F%91%8D
The RFC also says producers "should use uppercase hexadecimal digits for all percent-encodings." %c3%a9 means the same thing, but uppercase is the convention.
%20 vs +: two rule sets
Here's the confusing part. There are two encodings in common use:
- Percent-encoding (RFC 3986): a space is
%20, everywhere in a URL. - Form encoding (
application/x-www-form-urlencoded): a space is+. This is how HTML forms submit data, and it's defined in the WHATWG URL Standard.
The same text in each, using the three modes of our URL Encoder:
Input: café & croissants: 2 for €5!
Parameter value: caf%C3%A9%20%26%20croissants%3A%202%20for%20%E2%82%AC5!
Whole URL: caf%C3%A9%20&%20croissants:%202%20for%20%E2%82%AC5!
Form data: caf%C3%A9+%26+croissants%3A+2+for+%E2%82%AC5%21
The practical rules:
%20is valid for a space anywhere in a URL: path, query or fragment.+means a space only in form-encoded query strings and request bodies. In a path,/my+file.pdfis a file literally namedmy+file.pdf.- Because
+means space in form data, a real plus sign there must be encoded as%2B. Otherwisec++arrives ascwith two spaces.
Most server frameworks decode query strings as form data, so + and %20 both end up as spaces there. When in doubt, %20 is the safer choice for spaces and %2B for plus signs.
encodeURIComponent vs encodeURI
JavaScript has two built-in functions, and choosing the wrong one is a common bug.
encodeURIComponent: for one piece of a URL
encodeURIComponent encodes everything except letters, digits and - _ . ! ~ * ' ( ). It's what you want for a single parameter value or path segment, because it escapes the characters that would otherwise be read as structure:
encodeURIComponent("a/b?c=d#e") → a%2Fb%3Fc%3Dd%23e
encodeURIComponent("rock & roll") → rock%20%26%20roll
encodeURI: for a whole URL that's already structured
encodeURI leaves the structural characters ; / ? : @ & = + $ , # alone and only escapes characters that can never appear in a URL, such as spaces and non-ASCII letters. Use it on a complete address that just has a few illegal characters in it.
The bug happens when encodeURI is used on a value:
encodeURI("https://example.com/search?q=rock & roll&page=2")
→ https://example.com/search?q=rock%20&%20roll&page=2
The & inside "rock & roll" wasn't encoded, so the server now sees three parameters: q=rock , a parameter named roll, and page=2. Encode each value with encodeURIComponent before assembling the URL, and you get q=rock%20%26%20roll.
URLSearchParams: the easiest option
In modern JavaScript, let URLSearchParams build query strings for you. It encodes each value correctly using form encoding:
new URLSearchParams({ q: "rock & roll", city: "São Paulo" }).toString()
→ q=rock+%26+roll&city=S%C3%A3o+Paulo
Note the + for spaces: that's form encoding, and it's what servers expect in a query string.
The same choice in PHP and Python
Other languages offer the same two flavors, usually as a pair of functions:
| Language | Space as %20 | Space as + |
|---|---|---|
| PHP | rawurlencode() | urlencode(), http_build_query() |
| Python | urllib.parse.quote() | quote_plus(), urlencode() |
For a b+c/d, PHP's rawurlencode() gives a%20b%2Bc%2Fd and urlencode() gives a+b%2Bc%2Fd. Watch out for one difference in Python: quote() leaves / unencoded by default, returning a%20b%2Bc/d, because it assumes you're encoding a path. Pass safe="" when you're encoding a single value.
Double encoding: where %2520 comes from
If you encode text that's already encoded, the % signs get encoded too. A space becomes %20, and encoding that again turns the % into %25:
a b → a%20b → a%2520b
RFC 3986 warns about this directly: implementations "must not percent-encode or decode the same string more than once." It usually happens when two layers of code each try to be helpful, such as a URL builder that encodes values and a caller that encoded them first. If you see %25 followed by two hex digits in a URL, something encoded twice.
Decoding twice is just as bad. A value that legitimately contains %41 as text gets turned into A. Our URL Decoder shows you exactly what one round of decoding produces, which makes it easy to spot which layer is at fault.
Other mistakes to watch for
- Encoding a whole link to put it in another link. That's correct when the link is a parameter value, as in
?redirect=https%3A%2F%2Fexample.com%2F. It's wrong when you just want a working link, becausehttps%3A%2F%2Fno longer works as an address. - Decoding before splitting. Split a query string on
&and=first, then decode each name and value. Decoding first turns encoded%26into a real&that breaks the split. - Building URLs with string concatenation. Any user input that goes into a URL needs encoding. Use your language's URL builder instead of gluing strings together.
- Assuming non-ASCII domain names work the same way. Hostnames like
münchen.deuse a different system called Punycode, not percent-encoding.
When a URL misbehaves, our URL Parser splits it into host, path, parameters and fragment, so you can see exactly how a browser reads it. For a related encoding that turns binary data into text, see What Is Base64 Encoding?
FAQ
Should I use %20 or + for spaces?
Use %20 in paths and anywhere you're unsure. + is only correct in form-encoded query strings and form bodies, where most tools produce it automatically.
Do I need to encode hyphens, underscores, periods or tildes?
No. They're unreserved characters in RFC 3986 and can appear anywhere as-is.
Why are my non-English characters turned into several % codes?
Each one is converted to UTF-8 first, and many characters take 2 to 4 bytes in UTF-8. Each byte becomes one %XX code.
Is URL encoding a form of security?
No. It only makes data safe to put in a URL. Anyone can decode it.