Every web developer hits URL encoding bugs eventually. A redirect loop that only happens with certain usernames. A search feature that breaks when someone types C++. An API that returns garbage when the query contains emoji.
These bugs are annoying because they fail silently — the URL looks fine in the browser, but the server receives corrupted data.
Here are 5 encoding mistakes I've seen (and made) in production, and how to fix each one.
1. Using encodeURI when you need encodeURIComponent
This is the classic one. You're building a URL with user input:
// User searches for "node.js & deno"
const query = 'node.js & deno';
// WRONG — encodeURI preserves & and =
const bad = `https://api.example.com/search?q=${encodeURI(query)}`;
// "https://api.example.com/search?q=node.js%20&%20deno"
// Server sees: q = "node.js " and a second param " deno" = undefined
// RIGHT — encodeURIComponent encodes everything
const good = `https://api.example.com/search?q=${encodeURIComponent(query)}`;
// "https://api.example.com/search?q=node.js%20%26%20deno"
encodeURI() is designed for complete URLs — it preserves :, /, ?, &, =, #. If you use it on a value that contains &, the server interprets it as a parameter separator.
Rule: encodeURIComponent() for values. encodeURI() basically never (use the URL API instead).
2. Double encoding
This one is insidious. You encode a value, store it, then encode it again when building the URL:
const filename = 'my report.pdf';
const encoded = encodeURIComponent(filename); // "my%20report.pdf"
// Later, somewhere else in the codebase...
const url = `https://cdn.example.com/files/${encodeURIComponent(encoded)}`;
// "https://cdn.example.com/files/my%2520report.pdf"
// ^^^^ %25 is the encoded %
The % in %20 gets encoded to %25, producing %2520. The server decodes it to %20 (a literal string), not a space.
Fix: Encode raw values exactly once. If you're not sure whether a string is already encoded, decode first:
const safeEncode = (str) => encodeURIComponent(decodeURIComponent(str));
3. Forgetting that + and %20 are different
Two ways to encode a space. Two different standards.
| Encoding | Standard | Where it works |
|---|---|---|
%20 |
RFC 3986 (URI) | Everywhere |
+ |
HTML forms (application/x-www-form-urlencoded) |
Query strings only |
// These produce different output
encodeURIComponent('hello world') // "hello%20world"
new URLSearchParams({q: 'hello world'}).toString() // "q=hello+world"
Both are valid in query strings. But + in a path segment means a literal plus sign, not a space. I've seen this break file downloads where filenames contained spaces — the path used +, and the server treated it as a literal +.
Fix: Use %20 when in doubt. It works everywhere.
4. Not encoding path segments
// User uploads a file called "Q1 Report (Final).pdf"
const filename = 'Q1 Report (Final).pdf';
// WRONG
const url = `https://cdn.example.com/files/${filename}`;
// Spaces and parentheses in the path = broken
// RIGHT
const url = `https://cdn.example.com/files/${encodeURIComponent(filename)}`;
// "https://cdn.example.com/files/Q1%20Report%20(Final).pdf"
Most developers remember to encode query parameters but forget that path segments need encoding too. Any user-generated filename, category name, or slug with special characters will break without encoding.
5. Encoding the entire URL with encodeURIComponent
The opposite extreme — encoding everything:
const url = 'https://example.com/search?q=hello';
// Don't do this
encodeURIComponent(url)
// "https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dhello"
// This is not a valid URL anymore
This breaks the URL structure by encoding ://, /, ?, and =. I've seen this in redirect handlers where the full URL was passed through encodeURIComponent "for safety."
Fix: If you need to pass a URL as a parameter value, encode the whole URL with encodeURIComponent. If you need to navigate to a URL, don't encode its structural characters — use the URL API:
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'hello world & more');
url.searchParams.set('redirect', 'https://other.com/page?id=1');
url.toString()
// Everything is encoded correctly, automatically
The modern solution: just use the URL API
Honestly, most of these bugs disappear if you stop string-concatenating URLs:
// Instead of manual encoding...
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', userInput);
url.searchParams.set('page', '1');
url.searchParams.set('callback', 'https://mysite.com/done?status=ok');
fetch(url) // All encoding handled automatically
The URL and URLSearchParams APIs handle encoding correctly in every case. They're available in all modern browsers and Node.js.
If you want to quickly test how different strings get encoded (or decode a messy URL from server logs), I built a free URL Encoder/Decoder that runs entirely in your browser — nothing gets sent to a server.
For a complete reference of all percent-encoded characters, there's also a URL Encoding Cheat Sheet you can bookmark.
What encoding bugs have bitten you? I'd love to hear your war stories in the comments.
Top comments (0)