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.

Try it now: Use the URL Encoder / Decoder tool to encode and decode URLs instantly in your browser.

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:

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:

  1. 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.
  2. 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
Tip: The space character can be encoded as either %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:

CategoryCharactersEncoding Rule
UnreservedA-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 elseSpaces, < >, ", \, { }, non-ASCIIMust 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:

FunctionEncodesPreserves (does NOT encode)Use When
encodeURIComponent()All reserved and unsafe charactersA-Z a-z 0-9 - _ . ! ~ * ' ( )Encoding a query parameter value
encodeURI()Unsafe characters onlyAll 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!
Common bug: If your parameter value contains & 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:

CharacterUnicodeUTF-8 BytesURL Encoded
éU+00E90xC3 0xA9%C3%A9
U+4F600xE4 0xBD 0xA0%E4%BD%A0
U+754C0xE7 0x95 0x8C%E7%95%8C
🌍U+1F30D0xF0 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

  1. Framework auto-encoding: Many HTTP clients (Axios, fetch, requests) auto-encode URL parameters. If you already called encodeURIComponent manually, the framework encodes it again.
  2. Reverse proxy rewriting: Nginx or Cloudflare may re-encode URLs during proxy_pass.
  3. Redirect chains: Server A redirects to Server B with an encoded URL; Server B encodes it again.
Debugging tip: If you see %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:

AspectURL EncodingBase64 Encoding
PurposeMake text safe inside URLsMake binary data safe in text channels
InputText (usually already UTF-8)Any binary data
Output%XX sequences mixed with safe charsContinuous A-Za-z0-9+/= string
ExpansionOnly special chars expand (3×)Always ~33% larger
Use togetherBase64 output may contain + / = — URL-encode these if embedding in a URL, or use base64url variant
Related tools: URL Encoder / Decoder for percent-encoding, Base64 Encoder / Decoder for binary-to-text encoding, JWT Decoder for tokens that combine both encodings.

10. Quick Reference

ScenarioUseExample
Query parameter valueencodeURIComponent?q=hello%20world
Full URL as-isencodeURIencodeURI("https://example.com/path?q=1")
URL inside a URLencodeURIComponent (on inner URL)?redirect=https%3A%2F%2F...
OAuth redirect_uriencodeURIComponentredirect_uri=https%3A%2F%2F...
Chinese / non-ASCIIencodeURIComponent%E4%BD%A0%E5%A5%BD for 你好
HTML form submissionapplication/x-www-form-urlencoded (spaces → +)name=John+Doe
Base64 in URLUse base64url variant (- _ instead of + /)JWT tokens