URL Encoding Explained — Percent Encoding, encodeURIComponent vs encodeURI, and Common Pitfalls
URL encoding (also called percent encoding) is the mechanism that makes any text safe to include in a URL. Every character that is not a letter, digit, or one of a few "unreserved" symbols gets replaced by a percent sign followed by two hexadecimal digits — %20 for space, %2F for slash, %E4%BD%A0 for the Chinese character 你. If you've ever debugged a broken query string or chased a double-encoding bug, this guide is for you.
1. Why URL Encoding Exists
URLs have a strict grammar defined in RFC 3986. Certain characters serve as delimiters that give the URL its structure:
:separates the scheme (https:)/separates path segments?marks the start of the query string&and=separate query parameters#introduces the fragment identifier@separates user info from the host in authority components
If these characters appear in your data (not as delimiters), they must be percent-encoded — otherwise the parser can't tell the difference between a slash that's part of a path and a slash that's inside a parameter value.
2. How Percent Encoding Works
The encoding is straightforward:
- Convert the character to UTF-8 bytes. For ASCII characters (
!=0x21), this is one byte. For multi-byte characters (你 =0xE4 0xBD 0xA0), it can be 2–4 bytes. - Replace each byte with
%followed by the two-digit uppercase hex representation.
Character UTF-8 Bytes Percent-Encoded ───────── ────────── ──────────────── space 0x20 %20 ! 0x21 %21 / 0x2F %2F = 0x3D %3D 你 0xE4 0xBD 0xA0 %E4%BD%A0 🌍 0xF0 0x9F 0x8C 0x8D %F0%9F%8C%8D
%20 or + in the query string (application/x-www-form-urlencoded), but %20 is the correct RFC 3986 form. The + convention comes from HTML form submission and only applies within query strings — never in the path component.3. Unreserved vs Reserved Characters
RFC 3986 classifies URL characters into two groups:
| Category | Characters | Encoding Rule |
|---|---|---|
| Unreserved | A-Z a-z 0-9 - _ . ~ | Never need encoding — safe in any URL component |
| Reserved (gen-delims) | : / ? # [ ] @ | Have structural meaning — encode if used as data |
| Reserved (sub-delims) | ! $ & ' ( ) * + , ; = | Have meaning in specific components — encode if used as data |
| Everything else | Spaces, < >, ", \, { }, non-ASCII | Must always be encoded |
4. encodeURIComponent vs encodeURI — The Critical Difference
JavaScript provides two built-in encoding functions, and choosing the wrong one is the single most common URL encoding bug:
| Function | Encodes | Preserves (does NOT encode) | Use When |
|---|---|---|---|
encodeURIComponent() | All reserved and unsafe characters | A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Encoding a query parameter value |
encodeURI() | Unsafe characters only | All above PLUS ; , / ? : @ & = + $ # | Encoding a full URL |
Practical Example
// You want to build: https://api.example.com/search?q=hello world&page=2 const query = "hello world"; const url = "https://api.example.com/search?q=" + encodeURIComponent(query) + "&page=2"; // Result: https://api.example.com/search?q=hello%20world&page=2 ✓ Correct // WRONG — using encodeURI on a parameter value: const bad = "https://api.example.com/search?q=" + encodeURI(query) + "&page=2"; // Result: https://api.example.com/search?q=hello%20world&page=2 (looks OK for spaces) // But the bug shows up with special characters: const tricky = "a=1&b=2"; // a value that contains & and = const encoded = encodeURIComponent(tricky); // "a%3D1%26b%3D2" ✓ Safe const badEncoded = encodeURI(tricky); // "a=1&b=2" ✗ Breaks query parsing!
& or = and you use encodeURI, the server will interpret those characters as query string delimiters, splitting your value in half. Always use encodeURIComponent for individual parameter values.5. UTF-8 Multi-byte Encoding
Non-ASCII characters are encoded as their UTF-8 byte sequences, with each byte becoming a %XX triplet:
| Character | Unicode | UTF-8 Bytes | URL Encoded |
|---|---|---|---|
| é | U+00E9 | 0xC3 0xA9 | %C3%A9 |
| 你 | U+4F60 | 0xE4 0xBD 0xA0 | %E4%BD%A0 |
| 界 | U+754C | 0xE7 0x95 0x8C | %E7%95%8C |
| 🌍 | U+1F30D | 0xF0 0x9F 0x8C 0x8D | %F0%9F%8C%8D |
A Chinese URL like https://example.com/搜索?q=你好 becomes:
https://example.com/%E6%90%9C%E7%B4%A2?q=%E4%BD%A0%E5%A5%BD
6. Double Encoding — The Most Common Bug
Double encoding happens when an already-encoded string gets encoded again. Each % becomes %25, producing a cascade:
Original: hello world First encode: hello%20world Second encode: hello%2520world ← Bug! The server sees literal "%20" in the output
How Double Encoding Happens
- Framework auto-encoding: Many HTTP clients (Axios, fetch, requests) auto-encode URL parameters. If you already called
encodeURIComponentmanually, the framework encodes it again. - Reverse proxy rewriting: Nginx or Cloudflare may re-encode URLs during proxy_pass.
- Redirect chains: Server A redirects to Server B with an encoded URL; Server B encodes it again.
%25 in your URL, that's a percent sign that has been encoded — a clear sign of double encoding. Decode the URL once to check if the underlying %XX sequences are valid.7. Special Cases and Edge Scenarios
OAuth 2.0 Redirect URIs
The OAuth 2.0 spec requires that the redirect_uri parameter is encoded once using encodeURIComponent. The resulting URL becomes a query parameter value in the authorization request:
// redirect_uri value: https://myapp.com/callback?code=abc123 // After encodeURIComponent: // redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fcode%3Dabc123
URL in a URL (Nested URLs)
When one URL is a parameter inside another, the inner URL must be fully encoded:
https://example.com/redirect?url=https%3A%2F%2Ftarget.com%2Fpage%3Fid%3D42
Data URIs
Data URIs use Base64 encoding (not URL encoding) for binary content. See the Base64 Encoding Guide for details.
8. URL Encoding in Practice — Code Examples
JavaScript
// Encode a single parameter value
const name = "John O'Brien";
const url = `/api/users?name=${encodeURIComponent(name)}`;
// → /api/users?name=John%20O'Brien
// Encode a full URL (e.g., for a redirect parameter)
const target = "https://example.com/search?q=hello&page=1";
const redirect = `/goto?url=${encodeURIComponent(target)}`;
// → /goto?url=https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dhello%26page%3D1
// Decode a parameter
const decoded = decodeURIComponent("hello%20world%21");
// → "hello world!"
Python
from urllib.parse import quote, quote_plus, unquote
# quote() = encodeURIComponent equivalent
# quote_plus() = form-encoded (spaces become +)
encoded = quote("hello world!") # 'hello%20world%21'
encoded_plus = quote_plus("hello world!") # 'hello+world%21'
# Encode a full URL for a parameter
redirect_uri = "https://myapp.com/callback"
param = quote(redirect_uri, safe='') # 'https%3A%2F%2Fmyapp.com%2Fcallback'
# Decode
decoded = unquote("hello%20world%21") # 'hello world!'
Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Java's URLEncoder encodes spaces as "+" (form-encoded style)
String encoded = URLEncoder.encode("hello world!", StandardCharsets.UTF_8);
// → "hello+world%21"
// For RFC 3986 %20-style encoding, replace + with %20:
String rfc3986 = URLEncoder.encode("hello world!", StandardCharsets.UTF_8)
.replace("+", "%20");
// → "hello%20world%21"
// Decode
String decoded = URLDecoder.decode("hello%20world%21", StandardCharsets.UTF_8);
// → "hello world!"
9. URL Encoding vs Base64 Encoding
URL encoding and Base64 encoding serve different purposes and are often confused:
| Aspect | URL Encoding | Base64 Encoding |
|---|---|---|
| Purpose | Make text safe inside URLs | Make binary data safe in text channels |
| Input | Text (usually already UTF-8) | Any binary data |
| Output | %XX sequences mixed with safe chars | Continuous A-Za-z0-9+/= string |
| Expansion | Only special chars expand (3×) | Always ~33% larger |
| Use together | Base64 output may contain + / = — URL-encode these if embedding in a URL, or use base64url variant | |
10. Quick Reference
| Scenario | Use | Example |
|---|---|---|
| Query parameter value | encodeURIComponent | ?q=hello%20world |
| Full URL as-is | encodeURI | encodeURI("https://example.com/path?q=1") |
| URL inside a URL | encodeURIComponent (on inner URL) | ?redirect=https%3A%2F%2F... |
| OAuth redirect_uri | encodeURIComponent | redirect_uri=https%3A%2F%2F... |
| Chinese / non-ASCII | encodeURIComponent | %E4%BD%A0%E5%A5%BD for 你好 |
| HTML form submission | application/x-www-form-urlencoded (spaces → +) | name=John+Doe |
| Base64 in URL | Use base64url variant (- _ instead of + /) | JWT tokens |