Skip to content
Encoding2026-06-227 min read

URL Encoding Explained: How to Handle Spaces, Chinese Characters & Special Symbols

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. For a quick encode or decode, an online url encoder offers Component / Full URI modes and an optional “+ as space” decode toggle — all in the browser.

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 — a url params parser can show you exactly how each parameter decodes. 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

CharacterEncodedContext
Space%20 or +Paths use %20, forms use +
&%26Must encode in query values (separates params)
=%3DMust encode in query values (separates key/value)
#%23Must encode (starts fragment identifier)
?%3FMust encode in path (starts query string)
/%2FMust encode in path segments (separates segments)
@%40Encode in path to avoid userinfo confusion
€%E2%82%AC3-byte UTF-8
🎉%F0%9F%8E%894-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 ComponentCharacters That Need Encoding
Path segment/, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =, space, non-ASCII
Query parameter value&, =, +, #, space, non-ASCII
FragmentSpace, 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.

Double encoding: the bug that looks “almost right”

A value that already contains %20 gets passed through encodeURIComponent again and becomes %2520. Downstream systems that decode once still see a literal %20 string; systems that decode twice may “fix” it — until a third hop appears. Symptoms:

  • Search boxes that show %20 or %2520 in the UI instead of spaces
  • Logs where Chinese becomes %25E4%25B8%25AD…
  • “Works in the browser address bar, fails in the API client” because one path decoded and the other did not

Discipline: encode at the boundary where a raw string becomes a URL component; decode at the boundary where a URL component becomes application data; never encode “just in case” in the middle. When debugging, paste the suspect string into the URL Encoder/Decoder and compare one-step vs round-trip output, then open the URL Params Parser on the full URL to see how the browser’s query parser interprets + and %20.

Path vs query: different reserved sets

encodeURI leaves ?, &, /, and # alone because it assumes you are encoding a whole URL. encodeURIComponent encodes those characters because it assumes you are encoding one component. Mixing them is a classic footgun: using encodeURI on a search keyword that contains & splits the query; using encodeURIComponent on a full URL turns https:// into an unusable blob. Prefer URL + URLSearchParams constructors when you can — they apply the right rules per slot.

FAQ

Is + always a space?

In application/x-www-form-urlencoded query strings, yes — + means space. In path segments, + is a literal plus unless your framework specially treats it. When you need a real + in a query value, emit %2B.

Should I encode the fragment (#…)?

Browsers are lenient with fragments, but proxies and analytics tools are not always. If the fragment carries structured data, percent-encode non-ASCII and reserved characters the same way you would for a query value.


Need to quickly encode or decode a URL component? The URL Encoder/Decoder tool on ToolVault 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.

Triage notes

The four common mistakes are ordered by real incident frequency: double encoding ranks first and is explicitly called the most common bug — you encode once, the framework encodes again, % becomes %25, and the server receives a literal %20 string. The dual representation of space gets its own section, with the confusion's root in one line: + is a literal plus in the path and a space in the query string, so pick one convention when generating URLs and keep it. The Chinese-encoding section does the math: three bytes per character and three encoded characters per byte, so one hanzi balloons into nine characters — which is why Chinese URLs get long so fast.