URL encoding bugs are frustrating because they often work in development but break in production, or work in one browser but not another. Here are the most common mistakes and how to avoid them.
Key Takeaways
- 1Use the right function: encodeURIComponent for values, encodeURI for URLs
- 2Never encode an already-encoded string
- 3Let URL APIs handle encoding when possible
- 4Test with special characters: &, =, ?, #, spaces, unicode
- 5Check server logs for the actual received values
"Implementations must not percent-encode or decode the same string more than once, as decoding an already decoded string might lead to misinterpreting a percent data octet as the beginning of a percent-encoding."
Mistake 1: Using encodeURI for Query Values
This is the most common encoding mistake, affecting an estimated 15-20% of web applications according to security audits. Developers often confuse encodeURI() with encodeURIComponent(), not realizing they have fundamentally different purposes.
// ❌ WRONG: encodeURI doesn't encode & and =
const value = 'Tom & Jerry';
const broken = 'https://example.com/search?q=' + encodeURI(value);
// "https://example.com/search?q=Tom & Jerry"
// Server receives: q=Tom, Jerry= (broken!)
// ✅ CORRECT: Use encodeURIComponent for values
const fixed = 'https://example.com/search?q=' + encodeURIComponent(value);
// "https://example.com/search?q=Tom%20%26%20Jerry"
// ✅ BEST: Use URL API
const url = new URL('https://example.com/search');
url.searchParams.set('q', value);The problem is that encodeURI() preserves URL structural characters like & and =. When these appear in your data, they break the URL by being interpreted as parameter separators. The URL API is the safest choice because it handles context-appropriate encoding automatically.
Another equally frustrating mistake is encoding strings that are already encoded. This creates double encoding, which can be hard to track down.
Mistake 2: Encoding Twice
Double encoding happens when you encode a string, then pass it through code that encodes again. The most insidious cases involve frameworks or libraries that encode automatically—you might not even realize a second encoding step exists.
// ❌ WRONG: Encoding manually then letting framework encode again
const value = encodeURIComponent('hello world');
const url = new URL('https://example.com');
url.searchParams.set('q', value); // Encodes the % signs!
// q=hello%2520world (double encoded)
// ✅ CORRECT: Let the API handle encoding
const url = new URL('https://example.com');
url.searchParams.set('q', 'hello world');
// q=hello+worldThe rule is simple: if you're using the URL API (or any framework with built-in encoding), pass raw unencoded values. Let the API handle encoding. Manual encoding is only needed when building URLs through string concatenation.
The opposite problem—forgetting to encode entirely—is just as common and often more visible because it breaks URLs immediately.
Mistake 3: Forgetting to Encode Special Characters
This mistake often surfaces in testing because developers test with simple strings like "hello" that don't need encoding. The bug appears in production when real users search for "Tom & Jerry" or "price < $100" and get errors or wrong results.
// ❌ WRONG: String concatenation without encoding
const search = 'price < $100';
const url = 'https://shop.com/search?q=' + search;
// Broken URL with unencoded < and $
// ❌ WRONG: Assuming JSON.stringify handles it
const params = JSON.stringify({ q: search });
// Still needs URL encoding after stringification
// ✅ CORRECT: Always encode parameter values
const url = new URL('https://shop.com/search');
url.searchParams.set('q', search);A common misconception is that JSON.stringify() handles URL encoding. It doesn't—JSON encoding and URL encoding are completely different. JSON handles quotes and special JSON characters; URL encoding handles URL special characters. You need both when sending JSON in a URL parameter.
Plus signs deserve special attention because their meaning changes depending on context.
Mistake 4: Not Handling Plus Signs
In query strings, + means space. This convention dates back to HTML form encoding. If your data contains a literal plus sign (like "C++"), and you don't encode it, your server receives "C " (C followed by two spaces).
// ❌ WRONG: Assuming + is always a space
const value = 'C++ programming';
// After form submission: "C programming" (+ became spaces)
// ✅ CORRECT: Encode + as %2B when you mean literal +
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'C++ programming');
// q=C%2B%2B+programming (+ encoded, space becomes +)The URL API handles this correctly: it encodes literal + as %2B and encodes spaces as +. When the server decodes, it correctly reconstructs "C++ programming". This is yet another reason to prefer the URL API over manual encoding.
Inconsistency in encoding approaches within a codebase leads to subtle, intermittent bugs.
Mistake 5: Mixing Encoding Styles
Codebases often accumulate URL-building code over time, written by different developers with different approaches. Some functions use manual concatenation, others use the URL API, and bugs appear when these interact. Establishing a consistent approach prevents this category of bugs entirely.
// ❌ WRONG: Half manual, half automatic
function buildUrl(base, params) {
let url = base + '?';
for (const [key, value] of Object.entries(params)) {
url += key + '=' + value + '&'; // No encoding!
}
return url.slice(0, -1);
}
// ✅ CORRECT: Consistent use of URL API
function buildUrl(base, params) {
const url = new URL(base);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url.href;
}The first function works fine until someone passes a value with an & in it. Adopt the URL API as your standard and refactor legacy code when you encounter it. The investment pays off in fewer production bugs.
Finally, don't forget the user experience side of encoding: displaying encoded values to users looks unprofessional and confusing.
Mistake 6: Not Decoding Before Display
Users shouldn't see hello%20world or Tom+%26+Jerry. When displaying URL data in your UI, ensure it's decoded. The URL API does this automatically, but if you're extracting values manually, you need to decode explicitly.
// ❌ WRONG: Showing encoded values to users
const search = new URLSearchParams(location.search);
document.getElementById('title').textContent =
'Results for: ' + search.get('q'); // Already decoded!
// But if you get the raw query string:
const raw = location.search.slice(1); // ?q=hello%20world
// ❌ Display shows: hello%20world
// ✅ CORRECT: URLSearchParams decodes automatically
const params = new URLSearchParams(location.search);
params.get('q'); // "hello world" (decoded)A subtle point: URLSearchParams.get() already returns decoded values. The mistake often happens when developers extract values manually using substring operations or regex, bypassing the automatic decoding. If you find yourself parsing query strings manually, stop and use URLSearchParams instead.
When things go wrong despite your best efforts, systematic debugging helps identify the root cause quickly.
Debugging Tips
When debugging encoding issues, your goal is to identify exactly where encoding goes wrong. This requires examining the URL at multiple points: after construction, after sending, and as received by the server. The code below provides a systematic approach.
// 1. Log the actual URL being sent
console.log('Request URL:', url.href);
// 2. Check what the server receives
// In server logs, look for the raw query string
// 3. Test with problematic characters
const testCases = [
'hello world', // space
'Tom & Jerry', // ampersand
'key=value', // equals
'path/to/file', // slash
'100% done', // percent
'#hashtag', // hash
'C++ code', // plus
'café', // unicode
'日本語', // CJK
'🎉', // emoji
];
// 4. Verify round-trip encoding
const original = 'Tom & Jerry';
const encoded = encodeURIComponent(original);
const decoded = decodeURIComponent(encoded);
console.assert(original === decoded, 'Round-trip failed!');The test cases list covers the most common problematic characters. Add these to your unit tests to catch encoding bugs before they reach production. The round-trip test is especially valuable—if encoding then decoding doesn't return the original value, something is fundamentally wrong with your encoding approach.