URL Encoding Explained: %20, +, and the Differences That Break APIs
A space can be represented as %20 or as + in a URL. These two representations look interchangeable but they're not. Using the wrong one in a query parameter, a form POST body, or an OAuth signature string is a subtle bug that silently corrupts data, breaks authentication, or causes cryptic 400 errors that are frustrating to diagnose.
Why URLs need encoding
URLs are restricted to a subset of ASCII characters. Many characters — spaces, special symbols, non-ASCII letters — cannot appear literally in a URL because they'd be ambiguous or break the URL's syntax. A ? marks the start of a query string. A & separates query parameters. A # starts a fragment. If these characters appear in your data, they must be escaped.
Percent-encoding (also called URL encoding) is the solution: replace the problematic character with a percent sign followed by its two-digit hexadecimal ASCII code. A space (ASCII 32, hex 20) becomes %20. A & (ASCII 38, hex 26) becomes %26. An @ (ASCII 64, hex 40) becomes %40.
Reserved vs unreserved characters
The URL specification (RFC 3986) divides characters into two categories:
Unreserved characters — safe to use anywhere in a URL without encoding:
A-Z a-z 0-9 - _ . ~Reserved characters — have special meaning in URL syntax and must be percent-encoded when they appear as data (not as their structural meaning):
: / ? # [ ] @ ! $ & ' ( ) * + , ; =Everything else — spaces, non-ASCII characters, ", <, >, |, and so on — must always be percent-encoded.
%20 vs +: the critical difference
Both %20 and + represent a space — but they're defined in different contexts and are not interchangeable.
%20 is the standard percent-encoding of a space character, defined by RFC 3986. It is valid anywhere in a URL where a space needs to be encoded: path segments, query strings, fragments.
+ as a space representation is defined only in the application/x-www-form-urlencoded format — the encoding used when HTML forms submit data via POST. It is specific to query strings in this format. A literal + in a URL that doesn't use form encoding means a plus sign, not a space.
| Context | Space encoding | Notes |
|---|---|---|
| URL path segment | %20 | Always percent-encode |
| URL query string (RFC 3986) | %20 | Standard encoding |
| HTML form POST body | + | application/x-www-form-urlencoded |
| HTML form GET (query string) | + | Browsers use form encoding for forms |
| OAuth 1.0 signature base string | %20 | Must use percent-encoding, not + |
| REST API query parameters | %20 | Use encodeURIComponent |
A real example of the bug
Your API receives a search query. The user searches for "hello world". You build the URL:
// Wrong — builds URL with literal space (invalid URL)
const url = `https://api.example.com/search?q=${query}`;
// Also wrong — uses form encoding, may break non-form APIs
const url = `https://api.example.com/search?q=${query.replace(' ', '+')}`;
// Correct — standard percent-encoding for URL query parameters
const url = `https://api.example.com/search?q=${encodeURIComponent(query)}`;
// Result: https://api.example.com/search?q=hello%20worldIf the server uses a framework that decodes + as a space (many do), both versions might work. But if the server uses strict RFC 3986 decoding, it interprets + as a literal plus sign. Your "hello world" search becomes "hello+world" — a search for a string that probably returns zero results.
encodeURIComponent vs encodeURI
JavaScript provides two encoding functions that are frequently confused:
encodeURIComponent() encodes everything except unreserved characters (A-Z a-z 0-9 - _ . ! ~ * ' ( )). Use this for encoding individual parameter values before adding them to a URL. It encodes ?, &, =, /, and every other reserved character.
encodeURI() encodes everything except unreserved characters AND the reserved characters that have structural meaning in a URL (: / ? # [ ] @ ! $ & ' ( ) * + , ; =). Use this to encode a complete URL that already has its structure in place. It will not encode the ? that starts the query string or the & between parameters.
const value = "price=10&discount=5%";
encodeURIComponent(value)
// → "price%3D10%26discount%3D5%25"
// Encodes = and & — correct for a parameter value
encodeURI("https://example.com/search?" + value)
// → "https://example.com/search?price=10&discount=5%25"
// Does NOT encode = and & — they're interpreted as URL structure
// This is wrong for our value! The = and & break the query stringThe rule: always use encodeURIComponent() for parameter values. Only use encodeURI() for encoding a complete, already-structured URL (and even then, it's rarely needed).
Double encoding: a common mistake
Double encoding happens when you encode a value that's already encoded. A space becomes %20, then %20 gets encoded to %2520 (because % encodes to %25).
// Original value
"hello world"
// Encoded once (correct)
"hello%20world"
// Encoded twice (wrong — the % gets encoded)
"hello%2520world"
// Server decodes it: "hello%20world" — a literal percent-twenty, not a spaceThis commonly happens when building URLs in multiple steps — encoding a parameter value, then encoding the whole query string again, or passing an already-encoded URL through another encoding function.
Watch for this in logging and debugging: If you see %25 where you expected %, you have double encoding. If you see %2520, the original value had a space that was percent-encoded, and then that encoding was encoded again.
Non-ASCII characters
Non-ASCII characters (accented letters, CJK characters, emoji) must be UTF-8 encoded first, then each byte percent-encoded.
"café"
→ UTF-8 bytes: 63 61 66 C3 A9
→ percent-encoded: "caf%C3%A9"
"😊"
→ UTF-8 bytes: F0 9F 98 8A
→ percent-encoded: "%F0%9F%98%8A"encodeURIComponent() in JavaScript handles this correctly — it UTF-8 encodes the character before percent-encoding. Don't try to do this manually.
Practical checklist
- Use
encodeURIComponent()for every query parameter value in JavaScript - Use
URLSearchParamsto build query strings — it handles encoding automatically - For OAuth signatures, use percent-encoding (
%20), not form encoding (+) - Never build URLs by string concatenation without encoding the parts that contain user data
- If you receive a URL from a user or external system, decode before processing, re-encode before forwarding
- When debugging, decode the URL first to see the actual values being sent
Use URLSearchParams: In modern JavaScript, new URLSearchParams({q: "hello world", page: 1}).toString() correctly produces q=hello+world&page=1 (form encoding). For REST APIs, use encodeURIComponent on each value and join manually, or use a URL building library.
// summary
%20is the standard percent-encoding of a space — valid anywhere in a URL+means space only inapplication/x-www-form-urlencodedform data — not in general URLs- Use
encodeURIComponent()for parameter values;encodeURI()only for full structured URLs - Double encoding (
%2520) happens when you encode already-encoded data — avoid multi-step encoding - Non-ASCII characters must be UTF-8 encoded first, then each byte percent-encoded
- Use
URLSearchParamsor a URL library instead of manual string concatenation