URL encoding trips up developers more often than it should. Not because it's complicated — it isn't — but because there are two different standards, multiple languages implement them slightly differently, and the difference between %20 and + for a space has caused real production bugs.
Let me break it down clearly.
Why URLs Need Encoding
HTTP URLs can only safely contain a subset of ASCII characters: letters, digits, and a handful of symbols (-._~:/?#[]@!$&'()*+,;=). Everything else — spaces, accented characters, &, =, #, and non-ASCII text — must be percent-encoded.
Percent-encoding replaces the unsafe character with % followed by the character's UTF-8 byte value in hexadecimal:
- Space →
%20 -
&→%26 -
=→%3D -
é→%C3%A9(two bytes in UTF-8)
Simple enough. The confusion starts when you need to choose which kind of encoding to apply.
encodeURIComponent vs encodeURI: The Core Distinction
JavaScript gives you two built-in functions, and mixing them up is the #1 URL encoding mistake:
encodeURIComponent(value) encodes everything except: A-Z a-z 0-9 - _ . ! ~ * ' ( )
That includes /, ?, =, &, #, @, and : — all URL structural characters.
encodeURI(url) encodes everything except URL structural characters: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
The rule
- Use
encodeURIComponentfor individual query parameter values - Use
encodeURIfor complete URLs where you only want to encode unsafe characters without touching the URL's structure
// Correct — encoding a query value
const query = "coffee & cake";
const url = `https://example.com/search?q=${encodeURIComponent(query)}`;
// → https://example.com/search?q=coffee%20%26%20cake
// Wrong — encodeURI doesn't encode & so the parameter breaks
const url2 = `https://example.com/search?q=${encodeURI(query)}`;
// → https://example.com/search?q=coffee%20&%20cake ← broken!
The %20 vs + Space Confusion
You'll see both %20 and + used for spaces in URLs. They're not interchangeable:
| Form | Standard | Valid In |
|---|---|---|
%20 |
RFC 3986 percent-encoding | All URL contexts |
+ |
HTML form encoding (application/x-www-form-urlencoded) |
Query strings only |
%20 is always correct. + is only correct in query strings when the server expects form-data encoding.
This matters when you're encoding a redirect URL as a parameter — a + inside a URL value that gets passed to another server and decoded as RFC 3986 will stay as a literal +, not become a space.
Language Cheat Sheet
JavaScript / Node.js:
// Query parameter values
encodeURIComponent("hello world & more") // hello%20world%20%26%20more
// Query strings with URLSearchParams
new URLSearchParams({ q: "hello world" }).toString() // q=hello+world (+ for spaces)
Python:
from urllib.parse import quote, urlencode
quote("hello world & more", safe="") # hello%20world%20%26%20more
urlencode({"q": "hello world"}) # q=hello+world (form encoding)
Java:
// URLEncoder uses + for spaces — this is form encoding, not RFC 3986
URLEncoder.encode("hello world", StandardCharsets.UTF_8) // hello+world
// Replace + with %20 if you need RFC 3986:
encoded.replace("+", "%20")
C#:
// Use Uri.EscapeDataString — produces %20 for spaces (RFC 3986)
Uri.EscapeDataString("hello world & more") // hello%20world%20%26%20more
// Avoid HttpUtility.UrlEncode — uses + for spaces
curl:
curl -G --data-urlencode "q=hello world & more" https://example.com/search
Double-Encoding: The Silent Bug
If you encode an already-encoded string, the % sign itself gets encoded to %25:
%20 (encoded space)
↓ encode again
%2520 (which decodes to "%20", not a space)
This is a common source of "double-encoded" URLs that confuse both users and servers. If your encoded string looks like %2520 instead of %20, you have a double-encoding bug.
Fix: always decode before re-encoding if the string might already be encoded.
When a URL Is a Parameter Value
This is the trickiest case — a redirect URL embedded inside another URL:
// Inner URL must be fully encoded so its ? & = / don't break the outer URL
const innerUrl = "https://destination.com/path?key=value&other=data";
const outerUrl = `https://proxy.example.com/redirect?target=${encodeURIComponent(innerUrl)}`;
// → https://proxy.example.com/redirect?target=https%3A%2F%2Fdestination.com%2Fpath%3Fkey%3Dvalue%26other%3Ddata
Only encode the value — not the outer URL's structure.
Quick Reference
Common characters and their encoded forms:
| Character | Encoded | Notes |
|---|---|---|
| Space | %20 |
+ only in form data |
& |
%26 |
Query string separator — always encode in values |
= |
%3D |
Key-value separator — encode in values |
? |
%3F |
Query start — encode in values |
/ |
%2F |
Path separator — encode in values |
# |
%23 |
Fragment — encode in values |
+ |
%2B |
Encode in values (means space in form encoding) |
% |
%25 |
Encode existing % to avoid double-encoding |
If you're working with URLs in the browser, there's a free URL Encoder / Decoder that handles both encodeURIComponent and encodeURI modes, plus batch processing for encoding lists of values at once — all client-side, so your URLs never leave the browser.
Top comments (0)