URL Encoding Demystified: Why %20 Exists
A URL looks simple until it breaks. Here's the definitive guide to percent-encoding, the difference between encodeURI and encodeURIComponent, and the gotchas that will ruin your API calls.
Every developer has hit this bug: you pass a search query to an API, and it works perfectly until
someone searches for "Tom & Jerry." The ampersand splits your query parameter in half. The server
receives q=Tom and a mysterious second parameter called Jerry.
URL encoding exists to prevent exactly this kind of chaos.
1. The Anatomy of a URL
Before we can understand encoding, we need to understand what a URL is made of. RFC 3986 defines the structure:
https://api.example.com:8080/v2/search?q=hello+world&lang=en#results
\___/ \_______________/\___/\________/ \__________________/ \_____/
scheme host port path query string fragment
Each part has its own rules about which characters are allowed. The ? separates the
path from the query. The & separates query parameters. The # marks the
fragment. The / separates path segments.
These characters are called reserved characters. If your data contains one of them, the URL parser can't tell the difference between structure and content. That's where percent-encoding comes in.
2. How Percent-Encoding Works
Percent-encoding replaces unsafe characters with a % followed by two hexadecimal
digits representing the character's ASCII (or UTF-8) byte value.
- Space →
%20(ASCII 32 = hex 20) &→%26(ASCII 38 = hex 26)=→%3D(ASCII 61 = hex 3D)#→%23(ASCII 35 = hex 23)/→%2F(ASCII 47 = hex 2F)
So "Tom & Jerry" in a query parameter becomes Tom%20%26%20Jerry. The URL parser now
sees the %26 as data, not as a parameter separator.
What About the + Sign?
In HTML form submissions (application/x-www-form-urlencoded), spaces are encoded as
+ instead of %20. Both are valid, but they belong to different specs.
%20 is the RFC 3986 standard. + is the HTML form convention. Most
servers accept both, but it's a common source of double-encoding bugs.
3. encodeURI vs. encodeURIComponent
JavaScript gives you two encoding functions, and using the wrong one is one of the most common web development mistakes.
encodeURI()
Encodes a full URL. It leaves structural characters alone
(: / ? # & =) because it assumes they're part of the URL's structure.
encodeURI("https://example.com/path?q=hello world")
// "https://example.com/path?q=hello%20world"
// ✅ The :// and ? are preserved
encodeURIComponent()
Encodes a single value within a URL. It encodes everything except
letters, digits, and - _ . ~ .
encodeURIComponent("hello world&foo=bar")
// "hello%20world%26foo%3Dbar"
// ✅ The & and = are escaped because they're DATA, not structure
The Golden Rule
Building a full URL from scratch? Use encodeURI(). Inserting a user-provided value
into a query parameter? Always use encodeURIComponent().
// ❌ WRONG: encodeURI won't escape the & in the user's input
const url = encodeURI(`https://api.com/search?q=${userInput}`);
// ✅ RIGHT: encode just the value
const url = `https://api.com/search?q=${encodeURIComponent(userInput)}`;
4. Unicode and Internationalized URLs
ASCII only covers 128 characters. What happens when someone searches for "café" or "日本語"?
The character is first converted to its UTF-8 byte sequence, and then each byte is percent-encoded individually:
- "é" → UTF-8 bytes:
C3 A9→%C3%A9 - "日" → UTF-8 bytes:
E6 97 A5→%E6%97%A5
Modern browsers hide this complexity by displaying decoded Unicode in the address bar while sending the encoded version over the wire. But if you're logging URLs or building them manually, you need to be aware of this multi-byte encoding.
5. Common Pitfalls
Double Encoding
If you encode a value that's already encoded, %20 becomes %2520 (the
% itself gets encoded to %25). The server receives a literal
%20 string instead of a space. Always encode raw values, never pre-encoded
strings.
Forgetting to Encode Path Segments
Most developers remember to encode query parameters but forget that path segments can contain
special characters too. A file named my report (final).pdf needs encoding in the
path:
/files/my%20report%20(final).pdf
API Keys in URLs
If your API key contains + or / (common in Base64-encoded keys), they
must be percent-encoded when placed in a URL. Otherwise the / creates a false path
segment and the + becomes a space.
6. Building Query Strings Safely with URLSearchParams
Manually constructing query strings with string concatenation is error-prone — it is easy to
forget to encode a value, double-encode an already-encoded one, or misplace an ampersand.
The URLSearchParams API handles all of this correctly:
// Manual — fragile, easy to introduce bugs
const url = `https://api.com/search?q=${userQuery}&lang=${lang}&page=${page}`;
// URLSearchParams — handles encoding automatically
const params = new URLSearchParams({
q: userQuery, // automatically encodes spaces, &, =, etc.
lang: lang,
page: page
});
const url = `https://api.com/search?${params.toString()}`;
// Also useful for reading query strings
const input = "?q=Tom+%26+Jerry&lang=en";
const parsed = new URLSearchParams(input);
parsed.get('q'); // "Tom & Jerry" — decoded automatically
parsed.get('lang'); // "en"
URLSearchParams is available natively in all modern browsers and Node.js 10+.
It correctly handles the + vs %20 space convention in query
strings, serialises arrays, and decodes percent-encoded values on read. For any new code
that constructs or parses query strings, prefer URLSearchParams over manual
string manipulation.
7. Query String Arrays: The Framework Problem
There is no RFC standard for encoding arrays in query strings. Different frameworks have adopted incompatible conventions, which causes silent bugs when sending multi-value parameters to an API that expects a different format.
| Convention | Format | Used By |
|---|---|---|
| Repeated keys | ids=1&ids=2&ids=3 |
URLSearchParams, Java Servlet, Go |
| Bracket notation | ids[]=1&ids[]=2 |
PHP, Rails, jQuery.param() |
| Comma-separated | ids=1,2,3 |
Many REST APIs, OpenAPI |
| Index notation | ids[0]=1&ids[1]=2 |
ASP.NET, some PHP frameworks |
When integrating with a third-party API, always check its documentation for the expected
array format. When building your own API, document which convention you use. The repeated
keys format is most broadly supported by standard parsers and is what
URLSearchParams.append() produces by default.
Frequently Asked Questions
Why does + sometimes mean a space and sometimes not?
In query strings (the part after ?), + means a space under the
application/x-www-form-urlencoded convention — the encoding used by HTML
form submissions. This is a legacy convention from early web forms. In URL path segments
(before the ?), + is a literal plus sign and must be written
as %2B if you mean a plus in data. Outside query strings, always use
%20 for spaces. URLSearchParams handles this context correctly
when you use toString().
How do I detect double-encoding in a URL?
If you see %25 in a URL where you expected %, the value has been
double-encoded — the percent sign itself was encoded. You can also look for sequences like
%2520 (double-encoded space). The simplest fix is to ensure encoding happens
exactly once, on the raw value before any concatenation. If you are receiving URLs from
external sources, decode once with decodeURIComponent() and check whether
the result still contains percent-encoded sequences.
Should API keys be placed in the URL?
Not if you can avoid it. URL parameters are logged in server access logs, browser history,
proxy logs, and HTTP Referer headers — all places where secrets should not appear. Prefer
sending API keys in the Authorization header or a custom header (both are
excluded from most access logs and Referer leakage). If you must include a key in a URL
(some APIs require it), ensure it is percent-encoded if it contains any Base64 characters
like + or /.
Conclusion
URL encoding is invisible when it works and catastrophic when it doesn't. The rules are simple:
encode values with encodeURIComponent(), never double-encode, and test with special
characters like & = + # / and Unicode.
Need to debug a messy URL? Use our URL Encoder to encode, decode, and parse query strings instantly in your browser.