DEV Community

Snappy Tools
Snappy Tools

Posted on

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

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
// 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!
Enter fullscreen mode Exit fullscreen mode

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:

// 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Java:

// URLEncoder uses + for spaces — this is form encoding, not RFC 3986
URLEncoder.encode("hello world", StandardCharsets.UTF_8)  // hello+world
// Replace + with %20 if you need RFC 3986:
encoded.replace("+", "%20")
Enter fullscreen mode Exit fullscreen mode

C#:

// 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
Enter fullscreen mode Exit fullscreen mode

curl:

curl -G --data-urlencode "q=hello world & more" https://example.com/search
Enter fullscreen mode Exit fullscreen mode

Double-Encoding: The Silent Bug

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

%20  (encoded space)
  ↓ encode again
%2520 (which decodes to "%20", not a space)
Enter fullscreen mode Exit fullscreen mode

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:

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

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.

Top comments (0)