URL Encoding Mistakes Developers Still Make (and Fixes)

URL encoding bugs are subtle, persistent, and often discovered in production. A misplaced %20, a double-encoded ampersand, or a confused + vs space can break API calls, corrupt tracking data, and create security vulnerabilities. Let's fix the most common ones.
Test your encoding with our URL Encoder/Decoder tool — paste any string and see the encoded result instantly.
encodeURI vs encodeURIComponent
JavaScript has two built-in encoding functions, and using the wrong one is the #1 source of bugs:
| Function | Encodes | Preserves | Use for |
|---|---|---|---|
encodeURI | Spaces, non-ASCII | : / ? # [ ] @ ! $ & ' ( ) * + , ; = | Encoding a full URL |
encodeURIComponent | Everything above + : / ? # @ ! $ & ' ( ) * + , ; = | Only - _ . ~ and alphanumerics | Encoding a query parameter value |
// WRONG — encodeURI preserves & in query values
const url = 'https://api.example.com/search?q=' + encodeURI('tom & jerry');
// Result: https://api.example.com/search?q=tom%20&%20jerry
// The & is preserved — now it looks like a separate parameter!
// CORRECT
const url = 'https://api.example.com/search?q=' + encodeURIComponent('tom & jerry');
// Result: https://api.example.com/search?q=tom%20%26%20jerry
Rule of thumb: Use encodeURIComponent for individual values. Use encodeURI only when you have a complete URL that just needs non-ASCII characters encoded.
Double-Encoding Bugs
Double-encoding happens when an already-encoded string is encoded again:
const name = 'hello world';
const encoded = encodeURIComponent(name); // hello%20world
const doubled = encodeURIComponent(encoded); // hello%2520world
// %25 is the encoding of % itself!
This typically occurs when:
- A framework auto-encodes query parameters, and you pre-encode them too
- A URL is encoded before being stored, then encoded again when retrieved
- Middleware processes the URL at multiple layers, each adding encoding
Detection
Look for %25 in URLs — it's almost always a sign of double-encoding. The sequence %2520 (double-encoded space) is the classic telltale.
The + vs %20 Confusion
In HTML form submissions (application/x-www-form-urlencoded), spaces become +. In standard percent-encoding (RFC 3986), spaces become %20.
This matters because:
decodeURIComponentdoes not decode+back to space — it leaves it as a literal+- Backend frameworks may or may not auto-decode
+depending on the parser - If your API expects
%20but receives+, searches for "red+car" return results for "red+car" (with a literal plus) instead of "red car"
// Safe decoding that handles both + and %20
function safeDecodeParam(value) {
return decodeURIComponent(value.replace(/\+/g, '%20'));
}
UTF-8 Edge Cases
Non-ASCII characters like é, ü, 日本語, and emoji must be percent-encoded as their UTF-8 byte sequences:
encodeURIComponent('café') // caf%C3%A9
encodeURIComponent('日本語') // %E6%97%A5%E6%9C%AC%E8%AA%9E
encodeURIComponent('🔒') // %F0%9F%94%92
Problems arise when:
- The server expects Latin-1 but receives UTF-8 (mojibake — garbled characters)
- Database columns are not set to UTF-8, silently truncating multi-byte characters
- Log files interpret encoded strings with wrong charset
Redirect and Callback URL Pitfalls
OAuth callbacks and redirect URLs are especially prone to encoding bugs:
// Building an OAuth redirect
const redirectUri = 'https://myapp.com/callback?source=oauth';
const authUrl = `https://provider.com/auth?redirect_uri=${encodeURIComponent(redirectUri)}`;
// Correct: the entire callback URL (including its own ?) is encoded as a single parameter value
Common mistakes:
- Not encoding the
redirect_uriat all — the?in the callback splits the parent URL - Encoding the
redirect_uripartially — encoding the path but not the query - Encoding the entire auth URL instead of just the parameter value
These bugs often create open redirect vulnerabilities that attackers exploit for phishing.
Debug Checklist
When a URL isn't working as expected, check these in order:
- Look for %25 — indicates double-encoding
- Check + vs %20 — are spaces handled consistently?
- Inspect raw request — use browser DevTools Network tab to see the actual encoded URL sent
- Test with special characters — try
& = ? # /in values to see if they break the URL structure - Check Content-Type — is the server parsing as
application/x-www-form-urlencodedorapplication/json? - Verify decode at server — log the raw and decoded values on the server side
Safe Helper Functions
// Build a query string safely
function buildQueryString(params) {
return Object.entries(params)
.map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
)
.join('&');
}
// Use URLSearchParams (modern browsers + Node.js)
const params = new URLSearchParams({ q: 'tom & jerry', page: '1' });
const url = `https://api.example.com/search?${params}`;
// Correct: handles encoding automatically
Best practice: Use URLSearchParams or URL constructor instead of manual string concatenation. They handle encoding correctly by default.
FAQ
Why does + become a space?
This is a legacy from HTML form encoding (application/x-www-form-urlencoded), where spaces are encoded as +. In standard percent-encoding (RFC 3986), spaces are %20. The + convention only applies to query strings in form submissions, not to path segments or other URL parts.
How do I encode nested URLs correctly?
Use encodeURIComponent on the inner URL before placing it into the outer URL's query parameter. This encodes characters like :/? that would otherwise be interpreted as part of the outer URL's structure.
Why do signatures break after URL encoding?
Signatures are computed over exact byte sequences. If you sign a string before encoding (or after decoding), but verify it in encoded form (or vice versa), the bytes differ and the signature fails. Always normalize the encoding before signing.
Related Tools & Articles
- URL Encoder/Decoder — test encoding in real time
- JWT Security Mistakes — encoding matters for token handling too
- Checksum Mismatch Fix — when encoding changes corrupt file hashes