URL Encoding Demystified: Percent-Encoding, encodeURI, and When to Use What
Published March 2026 · 11 min read
1. Introduction
Every time you paste a link into your browser, submit a form, or call an API, URLs are doing heavy lifting behind the scenes. But URLs have a strict character diet. They were born in the ASCII era, and the original spec only allows a small subset of characters to appear literally. Everything else—spaces, accented letters, emoji, and even some punctuation—must be translated into a safe representation before the browser or server can process it.
That translation process is called URL encoding (or more precisely, percent-encoding). If you have ever seen %20 in a URL where a space should be, you have already encountered it. Get the encoding wrong and you end up with broken links, garbled query parameters, or subtle security vulnerabilities.
This guide covers the full picture: how percent-encoding works under the hood, the difference between JavaScript’s confusingly-named encodeURI and encodeURIComponent, the modern URL and URLSearchParams APIs, encoding in other languages, and the security pitfalls you need to watch out for. The definitive reference for all of this is RFC 3986 (Uniform Resource Identifier: Generic Syntax), published in 2005 and still the foundation of every URL you use today.
Who is this for? Any developer who builds web applications, works with REST APIs, or has ever been confused by a URL that “looks right” but breaks when you click it.
2. What is Percent-Encoding?
Percent-encoding is a mechanism for representing arbitrary bytes inside the limited ASCII character set allowed in URIs. The algorithm is straightforward:
- Take the character that needs encoding (e.g.
@). - Convert it to its UTF-8 byte sequence. For ASCII characters this is a single byte; for multi-byte characters like emoji it can be up to four bytes.
- Express each byte as
%followed by two uppercase hexadecimal digits.
// Examples of percent-encoding Space → %20 @ → %40 # → %23 & → %26 = → %3D / → %2F ? → %3F % → %25 (the percent sign itself must be encoded!) € → %E2%82%AC (3 UTF-8 bytes) 한 → %ED%95%9C (3 UTF-8 bytes) 😀 → %F0%9F%98%80 (4 UTF-8 bytes)
Reserved vs. Unreserved Characters
RFC 3986 splits URL characters into two groups. Understanding this distinction is the key to knowing what to encode and what to leave alone.
Unreserved characters can appear anywhere in a URL without encoding. They are:
A-Z a-z 0-9 - . _ ~
Reserved characters have special meaning in the URL structure. They act as delimiters between the scheme, authority, path, query, and fragment:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
When a reserved character appears outside its designated role (for example, a literal & inside a query parameter value rather than as a parameter separator), it must be percent-encoded.
Common Encoded Characters Reference
| Character | Encoded | Notes |
|---|---|---|
| (space) | %20 | Most common encoding |
| ! | %21 | Reserved sub-delimiter |
| # | %23 | Fragment delimiter |
| $ | %24 | Reserved sub-delimiter |
| & | %26 | Query parameter separator |
| ' | %27 | Reserved sub-delimiter |
| + | %2B | Often confused with spaces in forms |
| / | %2F | Path segment separator |
| : | %3A | Scheme / port separator |
| = | %3D | Key-value separator in queries |
| ? | %3F | Query string start |
| @ | %40 | Userinfo delimiter |
3. URL Anatomy
Before diving into encoding functions, it helps to understand which parts of a URL exist and why each part has different encoding rules. The generic URI syntax defined in RFC 3986 looks like this:
scheme://authority/path?query#fragment Example: https://api.example.com:8080/users/john doe/posts?tag=c++&page=1#comments │ │ │ │ │ scheme authority path query fragment
Encoding Rules per Component
| Component | Example | What to Encode |
|---|---|---|
| Scheme | https | Never encoded. Fixed set of characters. |
| Authority (host) | api.example.com:8080 | International domain names use Punycode (xn--), not percent-encoding. |
| Path | /users/john%20doe/posts | Encode spaces and special chars, but preserve / as a path separator. |
| Query | tag=c%2B%2B&page=1 | Encode values (and keys if needed). Preserve & and = as structural delimiters. |
| Fragment | #comments | Sent only to the client. Similar encoding rules to query. |
Key insight: Different URL components have different reserved characters. A / is structural in the path but just data inside a query value. This is exactly why JavaScript provides two different encoding functions—one for full URLs and one for individual components.
Let’s break down a more complex real-world URL to see every piece in action:
https://search.example.com/results?q=hello%20world&lang=en&redirect=https%3A%2F%2Fother.com%2Fpage#top
Breakdown:
scheme: https
host: search.example.com
path: /results
query key: q → value: hello%20world (space encoded)
query key: lang → value: en
query key: redirect → value: https%3A%2F%2Fother.com%2Fpage
↑ The entire nested URL is encoded as a single value
fragment: topNotice how the redirect parameter contains a full URL as its value. The colons and slashes in that nested URL must be encoded, otherwise the browser would interpret them as part of the outer URL’s structure.
4. JavaScript URL Encoding Functions
JavaScript gives you two pairs of encoding/decoding functions. Choosing the wrong one is the single most common source of URL encoding bugs in web development.
encodeURI() — Encode a Full URL
encodeURI() is designed to encode an entire URL string. It encodes characters that are not allowed anywhere in a URL (like spaces and non-ASCII characters) but deliberately preserves all structural delimiters so the URL remains valid:
// encodeURI preserves structural characters: : / ? # & = const url = "https://example.com/path name/page?q=hello world&lang=en#top"; encodeURI(url); // "https://example.com/path%20name/page?q=hello%20world&lang=en#top" // ✓ https:// preserved // ✓ / preserved (path separators intact) // ✓ ? preserved (query start intact) // ✓ & preserved (parameter separator intact) // ✓ = preserved (key-value separator intact) // ✓ # preserved (fragment start intact) // ✓ spaces encoded as %20
Characters that encodeURI does NOT encode: A-Z a-z 0-9 - _ . ~ ! * ' ( ) ; : @ & = + $ , / ? # [ ]
encodeURIComponent() — Encode a Single Value
encodeURIComponent() is designed to encode a single piece of data that will be placed inside a URL—typically a query parameter value or a path segment. It encodes everything that encodeURI does, plus all the structural delimiters:
// encodeURIComponent ALSO encodes : / ? # & =
const value = "hello world & goodbye";
encodeURIComponent(value);
// "hello%20world%20%26%20goodbye"
// ✓ spaces encoded
// ✓ & encoded (so it won't be mistaken for a param separator)
// Real use case: building a query string
const searchUrl =
"https://example.com/search?q=" + encodeURIComponent("price < $50 & in stock");
// "https://example.com/search?q=price%20%3C%20%2450%20%26%20in%20stock"Characters that encodeURIComponent does NOT encode: A-Z a-z 0-9 - _ . ~ ! * ' ( )
Side-by-Side Comparison
| Input | encodeURI() | encodeURIComponent() |
|---|---|---|
| hello world | hello%20world | hello%20world |
| a=1&b=2 | a=1&b=2 | a%3D1%26b%3D2 |
| https://x.com/p | https://x.com/p | https%3A%2F%2Fx.com%2Fp |
| price < $50 | price%20%3C%20$50 | price%20%3C%20%2450 |
| /path/to/file | /path/to/file | %2Fpath%2Fto%2Ffile |
When to Use Each
| Scenario | Use |
|---|---|
| You have a complete URL with spaces/unicode in it | encodeURI() |
| You are building a query string value | encodeURIComponent() |
| You are inserting a user-provided path segment | encodeURIComponent() |
| You are passing a URL as a query parameter value | encodeURIComponent() |
| You want automatic, safe handling (recommended) | URL / URLSearchParams |
Decoding
The corresponding decoding functions reverse the process:
decodeURI("https://example.com/path%20name?q=hello%20world");
// "https://example.com/path name?q=hello world"
decodeURIComponent("hello%20world%20%26%20goodbye");
// "hello world & goodbye"
// Be careful: decodeURIComponent on a full URL breaks it
decodeURIComponent("https%3A%2F%2Fexample.com");
// "https://example.com" ← correct, this was a single encoded value
decodeURIComponent("https://example.com/path%20name");
// "https://example.com/path name" ← works, but decodeURI is more appropriate hereWarning: Calling decodeURIComponent on a string that contains a bare % not followed by two hex digits will throw a URIError. Always wrap decoding calls in a try-catch when processing user-supplied input.
5. The URL and URLSearchParams APIs
Modern JavaScript (supported in all current browsers and Node.js) provides the URL and URLSearchParams classes. These handle encoding automatically and are now the recommended approach for URL manipulation.
The URL Constructor
const url = new URL("https://example.com/search");
// Access and modify components
url.pathname = "/api/users/john doe";
url.searchParams.set("q", "hello world & more");
url.searchParams.set("redirect", "https://other.com/page?x=1");
url.hash = "results";
console.log(url.toString());
// "https://example.com/api/users/john%20doe?q=hello+world+%26+more&redirect=https%3A%2F%2Fother.com%2Fpage%3Fx%3D1#results"
// Parse an existing URL
const parsed = new URL("https://example.com/path?a=1&b=hello%20world#frag");
console.log(parsed.hostname); // "example.com"
console.log(parsed.pathname); // "/path"
console.log(parsed.searchParams.get("b")); // "hello world" (automatically decoded)
console.log(parsed.hash); // "#frag"URLSearchParams
URLSearchParams provides a clean API for building, reading, and iterating over query strings without manual encoding:
// Building query strings from scratch
const params = new URLSearchParams();
params.set("search", "url encoding & decoding");
params.set("page", "1");
params.set("tags", "javascript");
params.append("tags", "web"); // multiple values for same key
console.log(params.toString());
// "search=url+encoding+%26+decoding&page=1&tags=javascript&tags=web"
// Parsing existing query strings
const fromString = new URLSearchParams("q=hello+world&lang=en");
console.log(fromString.get("q")); // "hello world"
console.log(fromString.has("lang")); // true
// Iterating
for (const [key, value] of fromString) {
console.log(`${key}: ${value}`);
}
// "q: hello world"
// "lang: en"
// Getting all values for a key
const multi = new URLSearchParams("color=red&color=blue&color=green");
console.log(multi.getAll("color")); // ["red", "blue", "green"]
// From an object
const fromObj = new URLSearchParams({
query: "test value",
limit: "10",
});
console.log(fromObj.toString()); // "query=test+value&limit=10"Why prefer URL/URLSearchParams? They handle encoding and decoding automatically, they never double-encode, and they produce well-formed URLs. You eliminate an entire class of bugs by letting the browser do the encoding for you.
One important behavior to note: URLSearchParams encodes spaces as + rather than %20 in its toString() output. This follows the application/x-www-form-urlencoded format, which is standard for HTML form submissions and query strings. Both + and %20 are valid representations of a space in a query string.
6. Common Encoding Pitfalls
Double Encoding
The most common mistake. If a string is already encoded and you encode it again, the % signs themselves get encoded:
const alreadyEncoded = "hello%20world"; // Double encoding turns %20 into %2520 encodeURIComponent(alreadyEncoded); // "hello%2520world" ← BROKEN! // The server will decode this to "hello%20world" (literal percent-two-zero) // Fix: only encode raw values, never pre-encoded strings const raw = "hello world"; encodeURIComponent(raw); // "hello%20world" ← correct
Forgetting to Encode Query Values
// BAD: user input inserted directly into URL
const userInput = "rock & roll";
const bad = `https://api.com/search?q=${userInput}&limit=10`;
// "https://api.com/search?q=rock & roll&limit=10"
// The server sees: q="rock ", unknown param " roll", limit="10"
// GOOD: encode the value
const good = `https://api.com/search?q=${encodeURIComponent(userInput)}&limit=10`;
// "https://api.com/search?q=rock%20%26%20roll&limit=10"
// BEST: use URLSearchParams
const url = new URL("https://api.com/search");
url.searchParams.set("q", userInput);
url.searchParams.set("limit", "10");
// "https://api.com/search?q=rock+%26+roll&limit=10"Plus Sign (+) vs. %20 for Spaces
This causes endless confusion. There are two conventions for encoding spaces in URLs, and which one is correct depends on where the space appears:
- In URL paths: Spaces must be
%20. The plus sign is a literal+character in paths. - In query strings: Both
%20and+represent a space. The+convention comes from theapplication/x-www-form-urlencodedformat used by HTML forms. - In form data: HTML forms always use
+for spaces when submitting withapplication/x-www-form-urlencoded.
// Path: space must be %20
"/files/my%20document.pdf" // ✓ correct
"/files/my+document.pdf" // ✗ this is a file literally named "my+document.pdf"
// Query string: both are valid
"?q=hello%20world" // ✓ works
"?q=hello+world" // ✓ also works
// encodeURIComponent uses %20
encodeURIComponent("hello world"); // "hello%20world"
// URLSearchParams.toString() uses +
new URLSearchParams({ q: "hello world" }).toString(); // "q=hello+world"Non-ASCII Characters
Unicode characters (accented letters, CJK characters, emoji) are encoded as their multi-byte UTF-8 representation. This can produce long encoded sequences:
encodeURIComponent("café"); // "caf%C3%A9"
encodeURIComponent("日本語"); // "%E6%97%A5%E6%9C%AC%E8%AA%9E"
encodeURIComponent("🚀"); // "%F0%9F%9A%80"
// Modern browsers display Unicode in the address bar
// but the actual HTTP request uses the encoded formFile Paths vs. URL Paths
File system paths and URL paths look similar but follow different rules. A Windows path like C:\Users\docs\file.txt uses backslashes and drive letters that have no meaning in URLs. Always convert file paths to URL paths explicitly, and never concatenate file paths directly into URLs.
7. URL Encoding in Different Languages
Every language has its own encoding utilities. The concepts are the same, but the function names and details differ.
JavaScript
// Encode a query parameter value
encodeURIComponent("hello world & more");
// "hello%20world%20%26%20more"
// Build a complete URL with URLSearchParams
const url = new URL("https://api.com/search");
url.searchParams.set("q", "hello world & more");
url.toString();Python
from urllib.parse import quote, urlencode
# Encode a single value (like encodeURIComponent)
quote("hello world & more", safe="")
# "hello%20world%20%26%20more"
# Build a query string from a dictionary
urlencode({"q": "hello world & more", "page": "1"})
# "q=hello+world+%26+more&page=1"
# Note: urlencode uses + for spaces (form encoding)
# Use quote_plus explicitly if you want + for spaces
# Use quote with safe="" if you want %20Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Encode a value (uses + for spaces, like form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"
// Decode
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"
// For path encoding (using %20 for spaces), use java.net.URI
URI uri = new URI("https", "example.com", "/path with spaces", "q=test", null);
// "https://example.com/path%20with%20spaces?q=test"Go
import "net/url"
// Encode a query value (+ for spaces)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"
// Encode a path segment (%20 for spaces)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"
// Build a complete URL
u, _ := url.Parse("https://api.com/search")
q := u.Query()
q.Set("q", "hello world & more")
u.RawQuery = q.Encode()
// "https://api.com/search?q=hello+world+%26+more"Notice the pattern: Every language distinguishes between encoding for paths (spaces become %20) and encoding for query strings/forms (spaces may become +). Make sure you are using the right function for your context.
8. URL Encoding in APIs
When working with REST APIs, encoding comes up constantly. Here are the most common scenarios and how to handle them correctly.
Query Parameters in GET Requests
Every value in a query string must be encoded. This includes search terms, filter values, pagination tokens, and anything else the user or your code supplies:
// Building an API request with fetch
const baseUrl = "https://api.example.com/v1/products";
const url = new URL(baseUrl);
url.searchParams.set("category", "Electronics & Gadgets");
url.searchParams.set("price_max", "100");
url.searchParams.set("sort", "price:asc");
url.searchParams.set("cursor", "eyJpZCI6MTIzfQ==");
const response = await fetch(url);
// Request URL:
// https://api.example.com/v1/products?category=Electronics+%26+Gadgets&price_max=100&sort=price%3Aasc&cursor=eyJpZCI6MTIzfQ%3D%3DForm Data in POST Requests
When you submit a form with Content-Type: application/x-www-form-urlencoded, the body is encoded the same way as a query string. The browser (or URLSearchParams) handles this for you:
// Sending form-encoded data
const formData = new URLSearchParams();
formData.set("username", "john@example.com");
formData.set("password", "p@ss w0rd!");
await fetch("https://api.example.com/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData.toString(),
// body: "username=john%40example.com&password=p%40ss+w0rd%21"
});Path Parameters with Special Characters
When user-supplied data appears in the URL path (not the query string), you need to encode each segment individually:
const username = "john/doe"; // contains a slash!
const repo = "my project";
// BAD: slash in username breaks the path
`https://api.github.com/repos/${username}/${repo}`
// "https://api.github.com/repos/john/doe/my project"
// Server interprets this as: repos → "john" → "doe" → "my project"
// GOOD: encode each path segment
`https://api.github.com/repos/${encodeURIComponent(username)}/${encodeURIComponent(repo)}`
// "https://api.github.com/repos/john%2Fdoe/my%20project"
// Server correctly sees: repos → "john/doe" → "my project"Pagination and Filters
APIs often return a “next page” URL or cursor token that may already be encoded. Do not re-encode it:
// API response includes a next_page URL
const apiResponse = {
data: [/* ... */],
next_page: "https://api.example.com/items?cursor=abc%3D%3D&limit=20"
};
// Use the URL directly — it's already encoded
await fetch(apiResponse.next_page);
// DON'T do this (double encoding)
await fetch(encodeURI(apiResponse.next_page));
// Would turn %3D into %253D — broken!9. Security Implications
URL encoding is not just a formatting concern—it has real security implications. Improper handling of encoded URLs can open the door to several attack vectors.
URL-Based Injection Attacks
If your server constructs URLs or HTML from user input without proper encoding, attackers can inject additional parameters, modify paths, or insert malicious content:
// Vulnerable: user input directly in URL
const userQuery = "test&admin=true"; // attacker adds extra parameter
const url = `/api/search?q=${userQuery}`;
// "/api/search?q=test&admin=true" ← unintended parameter injected!
// Safe: encode the value
const safeUrl = `/api/search?q=${encodeURIComponent(userQuery)}`;
// "/api/search?q=test%26admin%3Dtrue" ← treated as a single valueOpen Redirect Vulnerabilities
Many applications accept a redirect or return_url parameter to send users back to a specific page after login. An attacker can exploit this to redirect users to a malicious site:
// Attacker crafts this URL and sends it to a victim: https://trusted-site.com/login?redirect=https%3A%2F%2Fevil-site.com%2Fphishing // After login, the server redirects to: // https://evil-site.com/phishing // The user thinks they're still on trusted-site.com // Defense: validate redirect URLs server-side // - Only allow relative paths // - Whitelist allowed domains // - Never blindly redirect to user-supplied URLs
Path Traversal via Encoded Sequences
Attackers can use encoded directory traversal sequences to access files outside the intended directory:
// Attacker request: GET /files/%2E%2E%2F%2E%2E%2Fetc%2Fpasswd // If the server decodes this and uses it as a file path: // /files/../../etc/passwd → /etc/passwd // Double encoding can bypass naive filters: GET /files/%252E%252E%252F%252E%252E%252Fetc%252Fpasswd // First decode: /files/%2E%2E%2F%2E%2E%2Fetc%2Fpasswd // Second decode: /files/../../etc/passwd // Defense: // - Decode only once // - Normalize paths and reject anything containing ".." // - Use allowlists rather than denylists // - Serve files from a sandboxed directory
Security best practice: Always validate and sanitize on the server side. Client-side encoding is for correctness, not security. The server must never trust that a URL received from a client is properly encoded or free of malicious content. Decode exactly once, validate the result, then use it.
Summary of Security Rules
- Always encode user-supplied data before inserting it into URLs.
- Decode only once on the server. Multiple decode passes invite double-encoding attacks.
- Validate redirect URLs against a whitelist of allowed domains or restrict to relative paths.
- Reject or sanitize path traversal sequences (
..,%2E%2E) in file-serving endpoints. - Use established URL parsing libraries rather than string manipulation for building and validating URLs.
10. Try It Yourself
Theory is great, but nothing beats hands-on practice. Our URL encoding tool lets you encode and decode strings instantly, see the raw byte representation, and compare different encoding methods side by side.
URL Encode & Decode Tool
Paste any string to see it encoded in real time. Compare encodeURI vs encodeURIComponent, test edge cases, and decode percent-encoded strings instantly.
Open URL Encoder / Decoder