The first time I saw %20 in a URL, I thought something was broken. Like the server was glitching out and spitting garbage into my address bar. Turns out that "garbage" is how the web has worked since the beginning — it's just a space character wearing a disguise. After building web apps and debugging the weirdest URL-related bugs you can imagine, I've come to appreciate how elegant (and occasionally infuriating) percent-encoding really is. Let me save you some of the headaches I went through.
Why Do URLs Need Encoding?
URLs are defined by RFC 3986 and can only contain a limited set of characters: letters (A–Z, a–z), digits (0–9), and a small set of special characters: - _ . ~ ! * ' ( ). All other characters — including spaces, ampersands, equals signs, and non-ASCII characters — must be encoded before they can appear in a URL.
Without encoding, special characters can be misinterpreted by browsers and servers. For example:
- A space in a URL would break the URL structure entirely.
- An
&in a query value would be interpreted as a parameter separator. - A
=in a value would be interpreted as a key-value separator. - A
#would be treated as a fragment identifier.
How Percent-Encoding Works
Percent-encoding (also called URL encoding) replaces each unsafe character with a % sign followed by the two-digit hexadecimal representation of the character's UTF-8 byte value.
| Character | Encoded | Reason |
|---|---|---|
| Space | %20 | Spaces are not allowed in URLs |
| + | %2B | Plus sign has special meaning in query strings |
| & | %26 | Used as query parameter separator |
| = | %3D | Used as key-value separator in query strings |
| # | %23 | Marks the start of a URL fragment |
| / | %2F | Path separator — encode when in query values |
| ? | %3F | Marks the start of a query string |
| @ | %40 | Used in email addresses and auth URLs |
encodeURI vs encodeURIComponent
JavaScript provides two built-in functions for URL encoding, and choosing the wrong one is a common source of bugs:
encodeURI()
Encodes a complete URL. It does not encode characters that have special meaning in URLs (: / ? # [ ] @ ! $ & ' ( ) * + , ; =). Use this when encoding a full URL.
encodeURI("https://example.com/search?q=hello world")
// → "https://example.com/search?q=hello%20world"encodeURIComponent()
Encodes a URL component (like a query parameter value). It encodes all special characters including : / ? # & =. Use this when encoding individual parameter values.
const query = encodeURIComponent("hello & goodbye")
// → "hello%20%26%20goodbye"
const url = `https://example.com/search?q=${query}`Common URL Encoding Mistakes
⚠️ Double-encoding
Encoding an already-encoded URL results in %25 instead of %. Always decode first if the string might already be encoded.
⚠️ Using encodeURI for query values
encodeURI won't encode & and = which will break your query string. Always use encodeURIComponent for parameter values.
⚠️ Not encoding non-ASCII characters
Characters like é, ü, or Chinese characters must be UTF-8 encoded before percent-encoding. Modern browsers handle this automatically, but server-side code may not.
⚠️ Encoding the entire URL including protocol
Never encode the protocol (https://) or domain. Only encode path segments and query parameter values.
URL Encoding in Different Programming Languages
Every server-side language has its own functions for URL encoding, and they do not all behave identically. Knowing the right function to use in each language — and the subtle differences between them — prevents bugs that are notoriously hard to track down.
JavaScript
JavaScript provides two encoding functions. Use encodeURIComponent() for individual query parameter values and path segments. Use encodeURI() only when encoding a complete URL where you want to preserve the structural characters.
// Encoding a query parameter value
const query = "hello & goodbye"
const url = `https://example.com/search?q=${encodeURIComponent(query)}`
// → https://example.com/search?q=hello%20%26%20goodbye
// Building a query string from an object (recommended approach)
const params = new URLSearchParams({ q: "hello world", page: "2" })
const fullUrl = `https://example.com/search?${params.toString()}`
// → https://example.com/search?q=hello+world&page=2The URLSearchParams API is the modern, recommended way to build query strings in JavaScript. It handles encoding automatically and is available in all modern browsers and Node.js.
Python
Python's urllib.parse module provides comprehensive URL encoding tools. The quote() function encodes a string, and urlencode()encodes a dictionary of parameters into a query string.
from urllib.parse import quote, urlencode, quote_plus
# Encode a path segment (preserves /)
quote("/path/to/resource with spaces")
# → '/path/to/resource%20with%20spaces'
# Encode a query parameter value (encodes / as well)
quote("hello & goodbye", safe="")
# → 'hello%20%26%20goodbye'
# Build a query string from a dict
params = {"q": "hello world", "page": 2, "filter": "a&b"}
urlencode(params)
# → 'q=hello+world&page=2&filter=a%26b'PHP
PHP has two main URL encoding functions. urlencode() encodes spaces as + (suitable for query strings), while rawurlencode()encodes spaces as %20 (suitable for path segments, per RFC 3986).
// For query string values
urlencode("hello world & more") // → "hello+world+%26+more"
// For URL path segments
rawurlencode("hello world") // → "hello%20world"
// Build a query string from an array
http_build_query(["q" => "hello world", "page" => 2])
// → "q=hello+world&page=2"Java
Java's java.net.URLEncoder class handles URL encoding. Always specify the character encoding explicitly — StandardCharsets.UTF_8 is the correct choice for modern applications.
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// → "hello+world+%26+more"
// For building URLs, prefer URI builder
URI uri = new URIBuilder("https://example.com/search")
.addParameter("q", "hello world")
.addParameter("page", "2")
.build();Common Mistakes When Building Query Strings
⚠️ String concatenation without encoding
const url = "https://api.example.com/search?q=" + userInputFix: Use encodeURIComponent(userInput) or URLSearchParams — never concatenate raw user input into URLs.
⚠️ Encoding the entire URL
encodeURIComponent("https://example.com/path?q=hello")Fix: Only encode individual parameter values, not the full URL. Encoding the full URL will break the protocol and domain.
⚠️ Forgetting to decode on the server
Reading req.query.q without decodingFix: Most frameworks decode query parameters automatically, but be aware when reading raw request strings.
Encode and Decode URLs Online
URL Encoding Best Practices
URL encoding seems straightforward, but real-world applications introduce subtle issues that cause hard-to-debug problems. These best practices help you avoid the most common pitfalls when building, parsing, and transmitting URLs across systems.
When to Encode (and When Not To)
Encode individual query parameter values and path segments that contain user input or dynamic data. Do not encode the structural parts of a URL — the protocol, domain, port, path separators (/), query delimiter (?), and parameter separator (&) should remain as-is. The rule is simple: encode values, not structure.
// ✅ Correct — only encode the parameter values
const search = "hello & goodbye"
const url = `https://api.example.com/search?q=${encodeURIComponent(search)}&page=1`
// → https://api.example.com/search?q=hello%20%26%20goodbye&page=1
// ❌ Wrong — encoding the entire URL breaks its structure
const broken = encodeURIComponent("https://api.example.com/search?q=hello")
// → https%3A%2F%2Fapi.example.com%2Fsearch%3Fq%3Dhello
// ✅ Best practice — use URL/URLSearchParams API
const url = new URL("https://api.example.com/search")
url.searchParams.set("q", "hello & goodbye")
url.searchParams.set("page", "1")
// → https://api.example.com/search?q=hello+%26+goodbye&page=1Double-Encoding Pitfalls
Double-encoding happens when you encode an already-encoded string. The % character in %20 gets encoded again to %2520, resulting in broken URLs that display literal percent signs. This commonly occurs when middleware or multiple layers of code each apply encoding independently. To prevent it, encode once at the point of URL construction and pass the complete URL as-is through subsequent layers.
// Double-encoding example
const value = "hello world"
const encoded = encodeURIComponent(value) // → "hello%20world"
// ❌ Encoding again produces garbage
encodeURIComponent(encoded) // → "hello%2520world"
// ✅ Fix: check if already encoded before encoding
function safeEncode(str) {
try {
// If decoding produces a different string, it was already encoded
return decodeURIComponent(str) !== str ? str : encodeURIComponent(str)
} catch {
return encodeURIComponent(str)
}
}Testing Encoded URLs
Always test your URLs with characters that are commonly problematic: spaces, ampersands, plus signs, forward slashes, Unicode characters, and the percent sign itself. A URL that works with simple ASCII input may break when a user enters their name in Japanese, pastes a URL as a parameter value, or includes mathematical symbols. Use browser DevTools Network tab to inspect the actual encoded request URLs being sent.
Handling Unicode in URLs
Non-ASCII characters (Chinese, Arabic, emoji, accented letters) must be UTF-8 encoded before percent-encoding. Modern browsers handle this automatically in the address bar using Internationalized Resource Identifiers (IRI to URI conversion), but server-side code and APIs may not. Always ensure your backend decodes URL components as UTF-8, and useencodeURIComponent() which handles multi-byte characters correctly.
// Unicode characters are UTF-8 encoded then percent-encoded
encodeURIComponent("café") // → "caf%C3%A9"
encodeURIComponent("日本語") // → "%E6%97%A5%E6%9C%AC%E8%AA%9E"
encodeURIComponent("🎉") // → "%F0%9F%8E%89"
// Decoding reverses the process
decodeURIComponent("caf%C3%A9") // → "café"Framework Behavior Differences
Different frameworks handle URL encoding differently, which causes subtle bugs when systems interact. Express.js automatically decodes req.params andreq.query values. Django decodes path parameters but preserves query string encoding until you access the QueryDict. Spring Boot decodes path variables but not always query parameters. PHP's $_GET array is auto-decoded. When building APIs consumed by multiple clients, document whether parameter values should be sent encoded or raw, and test with each client library you support.
A common source of bugs is the + character: in query strings,+ is an alternative encoding for space (fromapplication/x-www-form-urlencoded), but in path segments it means a literal plus sign. JavaScript's encodeURIComponent() encodes spaces as %20, while URLSearchParams encodes them as +. Both are valid in query strings, but inconsistency between encoder and decoder causes data corruption.
URL Encoding Across Programming Languages
Every language handles URL encoding slightly differently. Here's the authoritative reference so you don't have to look this up every time you switch between projects:
// JavaScript (Browser & Node.js)
encodeURIComponent("hello world & more") // → "hello%20world%20%26%20more"
decodeURIComponent("hello%20world") // → "hello world"
new URLSearchParams({ q: "a&b", page: "1" }).toString() // → "q=a%26b&page=1"
// Python
from urllib.parse import quote, unquote, urlencode
quote("hello world & more") # → "hello%20world%20%26%20more"
unquote("hello%20world") # → "hello world"
urlencode({"q": "a&b", "page": "1"}) # → "q=a%26b&page=1"
// Java
import java.net.URLEncoder;
import java.net.URLDecoder;
URLEncoder.encode("hello world", "UTF-8") // → "hello+world" (uses + for space!)
URLDecoder.decode("hello+world", "UTF-8") // → "hello world"
// PHP
urlencode("hello world & more") // → "hello+world+%26+more" (+ for space)
rawurlencode("hello world & more") // → "hello%20world%20%26%20more" (RFC 3986)
urldecode("hello+world") // → "hello world"
// Go
import "net/url"
url.QueryEscape("hello world") // → "hello+world"
url.PathEscape("hello world") // → "hello%20world"
// C# (.NET)
Uri.EscapeDataString("hello world") // → "hello%20world"
HttpUtility.UrlEncode("hello world") // → "hello+world"Notice the inconsistency with spaces: JavaScript's encodeURIComponent uses%20, while Java's URLEncoder and PHP's urlencode use +. This is because + for spaces comes from HTML form encoding (application/x-www-form-urlencoded), while %20 follows RFC 3986. Both are valid in query strings, but mixing them between systems causes bugs.
Security Implications of URL Encoding
URL encoding isn't just about making URLs work — it's a critical security boundary. Improper handling leads to some of the most exploited web vulnerabilities:
🔴 Open Redirect Attacks
If your app redirects to a URL from a query parameter without validation, attackers can encode a malicious URL: ?redirect=https%3A%2F%2Fevil.com. Always whitelist allowed redirect domains.
🔴 Path Traversal via Double Encoding
Attackers send %252e%252e%252f (double-encoded ../). If your server decodes twice, it becomes a path traversal. Decode once, validate, never decode again.
🔴 SQL Injection via URL Parameters
URL decoding happens before your code processes the value. An encoded %27 becomes a single quote (') that can break SQL queries. Always use parameterized queries regardless of URL encoding.
🔴 XSS via Encoded Script Tags
Encoded payloads like %3Cscript%3Ealert(1)%3C%2Fscript%3E bypass naive input filters that only check for literal angle brackets. Always sanitize after decoding.
The golden rule: decode once, validate after decoding, never trust encoded input as "safe". URL encoding is transport-level escaping — it doesn't sanitize content.
Debugging URL Encoding Issues
After years of web development, I can tell you that URL encoding bugs are some of the most annoying to diagnose because the URL looks correct in the browser address bar (browsers decode it for display) but the raw request tells a different story. Here's my debugging playbook:
🔍 Search query returns no results despite exact match
Fix: Check if the query has a '+' that's being decoded as space when it should be literal. Use %2B for literal plus signs in search terms.
🔍 API returning 404 for a resource that exists
Fix: The resource name likely contains a slash or special character. Check if the path segment is properly encoded. '/users/alice/bob' vs '/users/alice%2Fbob' — same chars, different meaning.
🔍 Form submission with & in field values breaks parsing
Fix: Ensure the value is encoded before building the query string. 'q=Tom & Jerry' should be 'q=Tom%20%26%20Jerry'. Without encoding, it looks like two separate parameters.
🔍 Redirect loop after login with return URL
Fix: The return URL parameter is often double-encoded. If you encode '/dashboard?tab=main' into a query param and then it gets encoded again during form submission, you end up with '%252F' instead of '%2F'.
🔍 Non-Latin characters (Chinese, Arabic) corrupted in URL
Fix: Ensure both sender and receiver agree on UTF-8 encoding. The sender must UTF-8 encode first, then percent-encode. The receiver must percent-decode then UTF-8 decode.
Quick diagnostic: Open browser DevTools → Network tab → click the request → look at the "Request URL" field. This shows the actual encoded URL sent to the server. Compare it with what you expected. If special characters aren't properly encoded (or are double-encoded), you've found your bug.
Summary
Here's my top tip: encodeURIComponent() is your best friend. Use it for query parameter values, don't touch the structural parts of the URL, and never encode something that's already encoded. If you remember nothing else from this article, remember that one function. It'll save you from 90% of URL-related bugs. The other 10%? That's the + vs %20 situation, and honestly, that one still trips me up sometimes.
