← Back to Blog

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:

  1. Take the character that needs encoding (e.g. @).
  2. 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.
  3. 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

CharacterEncodedNotes
(space)%20Most common encoding
!%21Reserved sub-delimiter
#%23Fragment delimiter
$%24Reserved sub-delimiter
&%26Query parameter separator
'%27Reserved sub-delimiter
+%2BOften confused with spaces in forms
/%2FPath segment separator
:%3AScheme / port separator
=%3DKey-value separator in queries
?%3FQuery string start
@%40Userinfo 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

ComponentExampleWhat to Encode
SchemehttpsNever encoded. Fixed set of characters.
Authority (host)api.example.com:8080International domain names use Punycode (xn--), not percent-encoding.
Path/users/john%20doe/postsEncode spaces and special chars, but preserve / as a path separator.
Querytag=c%2B%2B&page=1Encode values (and keys if needed). Preserve & and = as structural delimiters.
Fragment#commentsSent 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:  top

Notice 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

InputencodeURI()encodeURIComponent()
hello worldhello%20worldhello%20world
a=1&b=2a=1&b=2a%3D1%26b%3D2
https://x.com/phttps://x.com/phttps%3A%2F%2Fx.com%2Fp
price < $50price%20%3C%20$50price%20%3C%20%2450
/path/to/file/path/to/file%2Fpath%2Fto%2Ffile

When to Use Each

ScenarioUse
You have a complete URL with spaces/unicode in itencodeURI()
You are building a query string valueencodeURIComponent()
You are inserting a user-provided path segmentencodeURIComponent()
You are passing a URL as a query parameter valueencodeURIComponent()
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 here

Warning: 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:

// 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 form

File 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 %20

Java

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%3D

Form 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 value

Open 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

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