URL encoding is one of those things that seems simple until you encounter a bug where a space in a query parameter breaks your entire API call. This guide explains the rules, the edge cases, and how to handle encoding correctly in every language.
What is URL Encoding?
URLs can only contain a limited set of characters defined in RFC 3986. Characters outside this set — spaces, Unicode letters, symbols, reserved characters used as delimiters — must be encoded as %XX where XX is the hexadecimal value of the byte.
This encoding is called percent encoding (commonly called "URL encoding").
For example:
- Space (
) →%20 #→%23&→%26=→%3D@→%40é→%C3%A9(UTF-8 bytes: 0xC3, 0xA9)
Safe vs. Reserved vs. Unreserved Characters
Unreserved characters — never need encoding:
A-Z a-z 0-9 - _ . ~
Reserved characters — have special meaning in URLs; encode them when used as data:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
Everything else — must be encoded, including spaces and non-ASCII characters.
Spaces: %20 vs +
This is the most common source of confusion:
%20is the RFC 3986 percent-encoded form of a space — valid everywhere in a URL+means a space only in HTML form data (application/x-www-form-urlencoded) — i.e., query strings submitted by HTML forms
In practice:
- If you're building a query string from an HTML form:
+is fine - If you're building a URL in code: use
%20(or use the right encoding function)
Encoding in JavaScript
JavaScript has three URL encoding functions, and they do different things:
// encodeURIComponent — encodes everything except: A-Za-z0-9 - _ . ! ~ * ' ( )
// USE THIS for encoding individual query parameter values
encodeURIComponent('hello world & more')
// → 'hello%20world%20%26%20more'
encodeURIComponent('user@example.com')
// → 'user%40example.com'
// encodeURI — leaves reserved characters and # unencoded
// USE THIS for encoding a full URL (preserves structure)
encodeURI('https://example.com/search?q=hello world')
// → 'https://example.com/search?q=hello%20world'
// escape() — DEPRECATED, do not use
// It doesn't handle non-ASCII correctly
The rule: use encodeURIComponent for values, encodeURI for full URLs.
// Building a query string correctly
const params = new URLSearchParams({
q: 'hello world',
filter: 'type=article&status=published',
tag: 'c++',
})
const url = `https://example.com/search?${params.toString()}`
// → https://example.com/search?q=hello+world&filter=type%3Darticle%26status%3Dpublished&tag=c%2B%2B
URLSearchParams is the modern, correct way to build query strings — it handles all encoding automatically.
Decoding:
decodeURIComponent('hello%20world%20%26%20more')
// → 'hello world & more'
decodeURIComponent('%C3%A9')
// → 'é'
Encoding in Python
from urllib.parse import quote, quote_plus, urlencode, unquote
# quote — encodes for URL paths (leaves / unencoded by default)
quote('hello world') # 'hello%20world'
quote('hello/world/path') # 'hello/world/path' ← / is kept
quote('hello/world', safe='') # 'hello%2Fworld' ← / is encoded
# quote_plus — encodes for query strings (space → +)
quote_plus('hello world') # 'hello+world'
quote_plus('a=1&b=2') # 'a%3D1%26b%3D2'
# Building a query string
params = {'q': 'hello world', 'page': 1, 'filter': 'a&b'}
urlencode(params)
# → 'q=hello+world&page=1&filter=a%26b'
# Decoding
unquote('hello%20world') # 'hello world'
unquote('%C3%A9') # 'é'
Encoding in the Terminal
# Using Python (available on virtually all systems)
python3 -c "import urllib.parse; print(urllib.parse.quote('hello world & more'))"
# hello%20world%20%26%20more
# Using curl --data-urlencode (for POST data)
curl -G "https://api.example.com/search" --data-urlencode "q=hello world & more"
# Using node
node -e "console.log(encodeURIComponent('hello world & more'))"
# hello%20world%20%26%20more
# Using jq
echo '"hello world & more"' | jq -r '@uri'
# hello%20world%20%2526%20more
Common Mistakes
Double encoding — encoding an already-encoded URL:
// WRONG: encodes %20 again → %2520
encodeURIComponent(encodeURIComponent('hello world'))
// → 'hello%2520world'
// Correct: decode first if input may already be encoded
const safe = decodeURIComponent(input) // decode
const encoded = encodeURIComponent(safe) // re-encode once
Encoding the whole URL including structure:
// WRONG: breaks the URL
encodeURIComponent('https://example.com/search?q=hello')
// → 'https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dhello'
// Correct: encode only the value
`https://example.com/search?q=${encodeURIComponent('hello')}`
Forgetting to encode user input in query parameters:
// WRONG: breaks if user types & or =
const url = `https://api.example.com?q=${userInput}`
// Correct
const url = `https://api.example.com?q=${encodeURIComponent(userInput)}`
URL Encoding vs Base64
Both encode data as ASCII text, but for different purposes:
- URL encoding → makes arbitrary text safe for use inside a URL
- Base64 → makes binary data safe for use inside text (JSON, HTML, email)
Don't use Base64 for URL parameters — the output contains +, /, and = which still need percent-encoding, making it worse than just URL-encoding the original value.
Try It Online
Use our free URL Encoder/Decoder to encode and decode URL strings instantly in your browser.
Summary
- Percent encoding converts unsafe characters to
%XXhex sequences - Use
encodeURIComponentfor individual values,encodeURIfor full URLs,URLSearchParamsfor query strings +means space only in form data — prefer%20in code- Never encode an already-encoded URL (causes double encoding →
%2520) - Always encode user-supplied input before inserting into URLs
References & Credits
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax — IETF, the definitive URI specification
- WHATWG URL Standard — WHATWG, defines how browsers parse URLs
- MDN Web Docs: encodeURIComponent() — Mozilla Developer Network
- MDN Web Docs: URLSearchParams — Mozilla Developer Network
- Python docs: urllib.parse — Python Software Foundation