Skip to content
encode2026-07-255 min read

URL encoding (formally called "percent-encoding") is one of those things developers encounter daily but rarely think about until something breaks. A space in a filename turns into %20, a Chinese character becomes a wall of percent signs, and somehow a + in a query parameter becomes a space on the server side. This article explains exactly how percent encoding works, why it exists, and how to handle the tricky cases without introducing bugs.

How Percent Encoding Works

URLs can only contain a limited set of ASCII characters: letters, digits, and a handful of special characters (-, _, ., ~). Everything else — spaces, punctuation, non-Latin scripts, emoji — must be encoded before it can safely appear in a URL.

The mechanism is straightforward:

  1. Take the character.
  2. Convert it to its UTF-8 byte representation.
  3. Replace each byte with % followed by its two-digit uppercase hexadecimal value.

For single-byte ASCII characters, this produces one percent-encoded triplet. For multi-byte characters (like Chinese, Japanese, or emoji), you get multiple triplets — one per byte.

Why Spaces Become %20 (or +)

The space character (ASCII 0x20) is the most commonly encoded character, and it has a confusing dual representation:

  • %20 — The standard percent-encoding for a space, valid anywhere in a URL (path, query, fragment).
  • + — A legacy encoding for spaces that is only valid in application/x-www-form-urlencoded data (i.e., form submissions and query strings).
# In a URL path, space MUST be %20:
https://example.com/files/my%20document.pdf

# In a query string, both work (but mean different things in some parsers):
https://example.com/search?q=hello%20world   # %20 = space
https://example.com/search?q=hello+world     # + = space (form encoding)

The confusion arises because + is a literal plus sign in URL paths but means "space" in query strings. If you're building an API that accepts query parameters, you need to handle both. If you're generating URLs, pick one convention and stick with it.

Encoding Chinese Characters (and Other Multi-Byte UTF-8)

Here's where things get interesting. The character (U+4E2D) is encoded in UTF-8 as three bytes: E4 B8 AD. So its percent-encoded form is:

中 → %E4%B8%AD

A full example:

Original:  你好世界
UTF-8:     E4 BD A0  E5 A5 BD  E4 B8 96  E7 95 8C
Encoded:   %E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C

Each Chinese character produces 9 characters when percent-encoded (3 bytes x 3 chars per byte). This is why URLs with Chinese text get long quickly.

Here's how this works in JavaScript:

// encodeURIComponent handles UTF-8 percent-encoding
encodeURIComponent('中')       // "%E4%B8%AD"
encodeURIComponent('你好')     // "%E4%BD%A0%E5%A5%BD"
encodeURIComponent('hello 世界') // "hello%20%E4%B8%96%E7%95%8C"

// decodeURIComponent reverses it
decodeURIComponent('%E4%B8%AD') // "中"

Other Common Special Characters

| Character | Encoded | Context | |-----------|---------|---------| | Space | %20 or + | Paths use %20, forms use + | | & | %26 | Must encode in query values (separates params) | | = | %3D | Must encode in query values (separates key/value) | | # | %23 | Must encode (starts fragment identifier) | | ? | %3F | Must encode in path (starts query string) | | / | %2F | Must encode in path segments (separates segments) | | @ | %40 | Encode in path to avoid userinfo confusion | | | %E2%82%AC | 3-byte UTF-8 | | 🎉 | %F0%9F%8E%89 | 4-byte UTF-8 (emoji) |

Common Mistakes and How to Avoid Them

Mistake 1: Double Encoding

The most common bug. You encode a value, then the framework encodes it again:

// BAD: double encoding
const param = encodeURIComponent('hello world');  // "hello%20world"
const url = `https://api.example.com/search?q=${encodeURIComponent(param)}`;
// Result: q=hello%2520world  (% got encoded to %25)
// Server receives: "hello%20world" (literal string, not a space)

The fix: encode once, at the point where you assemble the URL. Most HTTP libraries (axios, fetch with URLSearchParams) handle encoding for you:

// GOOD: let URLSearchParams handle encoding
const params = new URLSearchParams({ q: 'hello world', name: '张三' });
const url = `https://api.example.com/search?${params.toString()}`;
// Result: q=hello+world&name=%E5%BC%A0%E4%B8%89

Mistake 2: Mixing + and %20

If your backend decodes + as space but your frontend sends %20 (or vice versa), you'll get mismatches:

// Server expects form encoding (+ for space)
// But client sends:
fetch('/api?q=hello%20world')  // Some servers decode %20 correctly, others don't

// Safest: use URLSearchParams which produces consistent encoding
const params = new URLSearchParams();
params.set('q', 'hello world');
// Always produces: q=hello+world

Mistake 3: Encoding the Entire URL

Don't blindly encode a full URL — you'll encode the ://, /, ?, and & that are supposed to be structural:

// BAD: destroys URL structure
encodeURIComponent('https://example.com/path?q=hello world')
// "https%3A%2F%2Fexample.com%2Fpath%3Fq%3Dhello%20world"

// GOOD: encode only the dynamic parts
const baseUrl = 'https://example.com/path';
const query = encodeURIComponent('hello world');
const url = `${baseUrl}?q=${query}`;

Mistake 4: Assuming ASCII-Only

If your application handles internationalized input (usernames in Chinese, filenames in Arabic, search queries in Japanese), you must use UTF-8 percent-encoding. The old escape() function in JavaScript uses a non-standard encoding — never use it:

// NEVER use escape() — deprecated and non-standard
escape('中')  // "%u4E2D" — this is NOT valid percent-encoding

// Always use encodeURIComponent()
encodeURIComponent('中')  // "%E4%B8%AD" — correct UTF-8 percent-encoding

Practical Examples

Building a URL with Mixed Content

function buildSearchUrl(query, category, page) {
  const params = new URLSearchParams({
    q: query,          // "北京 weather" → q=%E5%8C%97%E4%BA%AC+weather
    category: category, // "food & drink" → category=food+%26+drink
    page: page
  });
  return `https://example.com/search?${params.toString()}`;
}

buildSearchUrl('北京 weather', 'food & drink', 1);
// "https://example.com/search?q=%E5%8C%97%E4%BA%AC+weather&category=food+%26+drink&page=1"

Decoding URL Parameters Safely

function getUrlParams(url) {
  const { searchParams } = new URL(url);
  const params = {};
  for (const [key, value] of searchParams) {
    params[key] = value;  // Already decoded by URL API
  }
  return params;
}

getUrlParams('https://example.com/search?q=%E4%BD%A0%E5%A5%BD&tag=a%2Bb');
// { q: "你好", tag: "a+b" }
// Note: %2B decodes to literal +, while + decodes to space

Handling Filenames with Special Characters

// Downloading a file with a Chinese name
const filename = '年度报告 2026.pdf';
const encodedFilename = encodeURIComponent(filename);
// "%E5%B9%B4%E5%BA%A6%E6%8A%A5%E5%91%8A%202026.pdf"

const downloadUrl = `/api/files/${encodedFilename}`;
// Server must decode the path segment to find the actual file

Quick Reference: What Needs Encoding Where

| URL Component | Characters That Need Encoding | |---------------|-------------------------------| | Path segment | /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =, space, non-ASCII | | Query parameter value | &, =, +, #, space, non-ASCII | | Fragment | Space, non-ASCII (browsers are lenient here) |

The simplest rule: if you're inserting a dynamic value into a URL, encode it with encodeURIComponent() (for path segments and query values) or use URLSearchParams (for query strings). Let the platform handle the details.


Need to quickly encode or decode a URL component? The URL Encoder/Decoder tool on DevToolkit Pro handles percent-encoding entirely in your browser — paste your string, get the encoded result instantly, no data sent to any server. It handles multi-byte UTF-8 characters, + vs %20 differences, and full URL encoding so you can verify your output before shipping it.


ad