URL Encoding Explained: %20, + Signs, and the encodeURIComponent vs encodeURI Confusion

4 58
calendar_today agoschedule4 min read

URL encoding trips up developers more often than it should. Not because it's complicated — it isn't — but because there are two different standards, multiple languages implement them slightly differently, and the difference between %20 and + for a space has caused real production bugs.

Let me break it down clearly.

Why URLs Need Encoding

HTTP URLs can only safely contain a subset of ASCII characters: letters, digits, and a handful of symbols (-._~:/?#[]@!$&'()*+,;=). Everything else — spaces, accented characters, &, =, #, and non-ASCII text — must be percent-encoded.

Percent-encoding replaces the unsafe character with % followed by the character's UTF-8 byte value in hexadecimal:

  • Space → %20
  • &%26
  • =%3D
  • é%C3%A9 (two bytes in UTF-8)

Simple enough. The confusion starts when you need to choose which kind of encoding to apply.

encodeURIComponent vs encodeURI: The Core Distinction

JavaScript gives you two built-in functions, and mixing them up is the #1 URL encoding mistake:

encodeURIComponent(value) encodes everything except: A-Z a-z 0-9 - _ . ! ~ * ' ( )

That includes /, ?, =, &, #, @, and : — all URL structural characters.

encodeURI(url) encodes everything except URL structural characters: : / ? # [ ] @ ! $ & ' ( ) * + , ; =

The rule

  • Use encodeURIComponent for individual query parameter values
  • Use encodeURI for complete URLs where you only want to encode unsafe characters without touching the URL's structure

``javascript

// Correct — encoding a query value

const query = "coffee & cake";

const url = https://example.com/search?q=${encodeURIComponent(query)};

// → https://example.com/search?q=coffee%20%26%20cake

// Wrong — encodeURI doesn't encode & so the parameter breaks

const url2 = https://example.com/search?q=${encodeURI(query)};

// → https://example.com/search?q=coffee%20&%20cake ← broken!

`

The %20 vs + Space Confusion

You'll see both %20 and + used for spaces in URLs. They're not interchangeable:

| Form | Standard | Valid In |

|------|----------|----------|

| %20 | RFC 3986 percent-encoding | All URL contexts |

| + | HTML form encoding (application/x-www-form-urlencoded) | Query strings only |

%20 is always correct. + is only correct in query strings when the server expects form-data encoding.

This matters when you're encoding a redirect URL as a parameter — a + inside a URL value that gets passed to another server and decoded as RFC 3986 will stay as a literal +, not become a space.

Language Cheat Sheet

JavaScript / Node.js:
`javascript
// Query parameter values


encodeURIComponent("hello world & more") // hello%20world%20%26%20more

// Query strings with URLSearchParams

new URLSearchParams({ q: "hello world" }).toString() // q=hello+world (+ for spaces)

`

Python:
`python
from urllib.parse import quote, urlencode

quote("hello world & more", safe="") # hello%20world%20%26%20more

urlencode({"q": "hello world"}) # q=hello+world (form encoding)

`

Java:
<code>java <p>// URLEncoder uses + for spaces — this is form encoding, not RFC 3986</p> <p>URLEncoder.encode("hello world", StandardCharsets.UTF_8) // hello+world</p> <p>// Replace + with %20 if you need RFC 3986:</p> <p>encoded.replace("+", "%20")</p> </code>

C#:
`csharp
// Use Uri.EscapeDataString — produces %20 for spaces (RFC 3986)


Uri.EscapeDataString("hello world & more") // hello%20world%20%26%20more

// Avoid HttpUtility.UrlEncode — uses + for spaces

`

curl:
<code>bash <p>curl -G --data-urlencode "q=hello world & more" https://example.com/search</p> </code>

Double-Encoding: The Silent Bug

If you encode an already-encoded string, the % sign itself gets encoded to %25:

<code> <p>%20 (encoded space)</p> <p>↓ encode again</p> <p>%2520 (which decodes to "%20", not a space)</p> </code>

This is a common source of "double-encoded" URLs that confuse both users and servers. If your encoded string looks like %2520 instead of %20, you have a double-encoding bug.

Fix: always decode before re-encoding if the string might already be encoded.

When a URL Is a Parameter Value

This is the trickiest case — a redirect URL embedded inside another URL:

<code>javascript <p>// Inner URL must be fully encoded so its ? & = / don't break the outer URL</p> <p>const innerUrl = "https://destination.com/path?key=value&other=data";</p> <p>const outerUrl = </code>https://proxy.example.com/redirect?target=${encodeURIComponent(innerUrl)}<code>;</p> <p>// → https://proxy.example.com/redirect?target=https%3A%2F%2Fdestination.com%2Fpath%3Fkey%3Dvalue%26other%3Ddata</p> </code>

Only encode the value — not the outer URL's structure.

Quick Reference

Common characters and their encoded forms:

| Character | Encoded | Notes |

|-----------|---------|-------|

| Space | %20 | + only in form data |

| & | %26 | Query string separator — always encode in values |

| = | %3D | Key-value separator — encode in values |

| ? | %3F | Query start — encode in values |

| / | %2F | Path separator — encode in values |

| # | %23 | Fragment — encode in values |

| + | %2B | Encode in values (means space in form encoding) |

| % | %25 | Encode existing % to avoid double-encoding |


If you're working with URLs in the browser, there's a free URL Encoder / Decoder that handles both encodeURIComponent and encodeURI` modes, plus batch processing for encoding lists of values at once — all client-side, so your URLs never leave the browser.

1.8k Points62 Badges4 58
75Posts
0Comments
SnappyTools builds free, fast, browser-based tools for developers, writers, and designers. No signup required, no data uploaded, no nonsense — just clean tools that work instantly ... Show more
Build your own developer journey
Track progress. Share learning. Stay consistent.
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

Sovereign Intelligence: The Complete 25,000 Word Blueprint (Download)

Pocket Portfolio - Apr 1

Just completed another large-scale WordPress migration — and the client left this

saqib_devmorph - Apr 7

Comparison: Universal Import vs. Plaid/Yodlee

Pocket Portfolio - Mar 12
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

8 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!