Why Your URL Encoding Is Breaking Your API (And How to Fix It)
Double-encoding, percent-sign bugs, and the silent API killers. Learn how URL encoding actually works and stop debugging mystery 400s.
If you've ever stared at a 400 Bad Request error and had no idea why - you're not alone. URL encoding bugs are one of the most frustrating, least-understood issues in web development. They don't throw loud errors. They silently corrupt your data.
This guide breaks down the real-world encoding mistakes that break APIs, with examples you can run in your browser right now using the Toolblip URL Encoder/Decoder.
What Is URL Encoding, Really?
URLs are designed to transmit raw bytes. But the characters in a URL have special meaning: ? marks the query string start, & separates parameters, / divides path segments, and # denotes a fragment. Any character that conflicts with those roles needs to be percent-encoded - converted to a % followed by two hexadecimal digits representing the byte value.
For example, a space (ASCII 32) becomes %20. An ampersand (&) becomes %26. A question mark (?) becomes %3F.
The full set of characters that don't need encoding - the unreserved set - is:
A-Z a-z 0-9 - _ . ~
Everything else should be encoded if it appears in a URL component where it has special meaning.
The Two Rules Most Developers Get Wrong
Rule 1: Encode Before Inserting Into a URL
This sounds obvious. But the mistake is doing it twice - or not at all.
Here's a typical flow:
// You want to search for "bread & butter"
const query = "bread & butter";
const url = `https://api.example.com/search?q=${query}`;
What actually gets sent:
GET /search?q=bread & butter HTTP/1.1
The & splits the query parameter. Your backend sees q=bread and a second parameter called butter with no value. The API might return 400, or worse - it might silently ignore the extra parameter and return wrong results.
The fix:
const query = "bread & butter";
const encoded = encodeURIComponent(query);
// "bread%20%26%20butter"
const url = `https://api.example.com/search?q=${encoded}`;
Now & becomes %26, and the space becomes %20. The URL is clean.
Rule 2: Don't Encode an Already-Encoded URL
This is the bug that causes double-encoding, and it trips up almost every developer at least once.
The problem happens when you encode data, then the value gets encoded again somewhere downstream:
const rawValue = "hello%20world";
const encoded = encodeURIComponent(rawValue);
// "hello%2520world" ← % becomes %25, double-encoded!
Now your server receives hello%2520world instead of hello%20world. The %20 you worked hard to encode got encoded again into %2520. Decode once, and you get hello%20world - still encoded. Decode twice, and finally: hello world.
Double-encoding is insidious because the URL looks valid. It passes syntax checks. The bug only shows up as corrupted data in your database or wrong search results.
The Three Most Common URL Encoding Bugs
Bug 1: Double-Encoding in Nested Systems
// Your code
const slug = encodeURIComponent("hello world"); // "hello%20world"
// A second, explicit encode is the bug:
const encodedAgain = encodeURIComponent(slug);
console.log(encodedAgain); // "hello%2520world"
Result: you look at your logs and see hello%2520world. You think something is wrong. Something encoded the already-encoded value a second time. Template interpolation and logging alone do not percent-encode it.
Fix: Track exactly where encoding happens in your stack. Encode at the boundary (when constructing the URL), and nowhere else.
Bug 2: Encoding the Wrong URL Component
encodeURIComponent() and encodeURI() are not interchangeable:
// Use encodeURIComponent() for individual query parameter values
const value = encodeURIComponent("a=b&c=d"); // "a%3Db%26c%3Dd"
// Use encodeURI() for full URLs you want to preserve structure
const url = encodeURI("https://example.com/path?a=b&c=d");
// "https://example.com/path?a=b&c=d" - separators preserved
If you use encodeURI() on a query value, you get the wrong result. If you use encodeURIComponent() on a full URL, you destroy the structure:
encodeURIComponent("https://example.com");
// "https%3A%2F%2Fexample.com" ← completely broken
Rule: encodeURIComponent() for component values. encodeURI() for full URLs you don't want re-encoded.
Bug 3: Forgetting to Decode on the Way In
This one is subtler. Your frontend encodes data correctly. But then a proxy, a load balancer, or a middleware layer does something unexpected:
// Backend receives: ?q=bread%26butter
// Frameworks commonly give you the decoded value:
const searchQuery = req.query.q; // "bread&butter" in this example
// Use parameterized queries when sending it to a database.
Decoding twice can corrupt a value containing literal percent escapes. SQL injection comes from building SQL with string interpolation, not from decodeURIComponent() itself.
Actually, in most modern frameworks the auto-decode is fine - but the danger is when you mix raw and decoded values, or when you store the encoded string but display the decoded one inconsistently.
How to Debug URL Encoding Issues
Step 1: Identify What You're Looking At
Is your string percent-encoded, or is it raw?
bread%20world → decoded: "bread world"
bread%2520world → double-encoded: "bread%20world" (needs one more decode)
bread world → raw: "bread world"
Step 2: Decode and Re-Encode
The fastest way to find the problem is to decode fully, then encode once:
// Fully decode
const decoded = decodeURIComponent(encodedString);
// If it's double-encoded, decode again:
const fullyDecoded = decodeURIComponent(decoded);
// Now encode once for the correct context
const reEncoded = encodeURIComponent(fullyDecoded);
You can do this instantly in your browser with the Toolblip URL Encoder/Decoder - paste a string, choose encode or decode, and copy the single output. To check another pass, paste that output back and run decode again.
Step 3: Check the Raw HTTP Request
Use your browser's Network tab or a tool like curl:
curl -v "https://api.example.com/search?q=bread%20butter"
Look at exactly what's being sent. Is %20 in the URL, or is it a literal space? The difference tells you where the encoding problem originates.
The Base64 URL-Safe Variant
Standard Base64 uses +, /, and = - characters that conflict with URL encoding. When you're putting Base64 in a URL parameter, you need the URL-safe variant:
Standard Base64: +/8= ← contains +, /, =
URL-safe Base64: -_8 ← replaces + and /, then removes padding
If you're putting tokens, signed data, or encoded payloads in URLs, URL-safe Base64 isn't optional - standard Base64 will break your URLs.
The Toolblip Base64 encoder handles standard Base64 text, without a URL-safe toggle. For base64url, replace + with -, / with _, and remove trailing = in code.
Trace an encoding bug
If a query arrives as C%252B%252B, decode one layer to get C%2B%2B, then another to get C++. Trace where each layer was added. Do not repeatedly decode unknown input until it “looks right”; a literal percent escape may be meaningful data.
Quick Reference
| Problem | Cause | Fix |
|---|---|---|
400 Bad Request on search queries |
Unencoded & or ? in query values |
encodeURIComponent() on values |
Data shows %2520 in logs |
Double-encoding | Decode once, re-encode once |
+ signs becoming spaces |
Form-encoded data interpreted as spaces | URL-encode + as %2B |
| Base64 in URLs breaking | Standard Base64 uses URL-unsafe chars | Use URL-safe Base64 (-_) |
| 404 on paths with special chars | / encoded inside path segment |
Don't encode slashes in path; only in values |
Try It Now
No signup. No server round-trips. Everything runs in your browser.
- URL Encoder/Decoder - encode, decode, and debug percent-encoded strings
- Base64 Encode/Decode - standard Base64 text conversion
Bottom Line
URL encoding bugs are invisible until they aren't. A + sign in a search query becomes a space. A % in a discount code becomes %25. A properly-encoded URL passes every syntax check but carries corrupted data.
The fix is knowing exactly where encoding happens in your stack - and making sure it happens exactly once, at the right layer. Once you have that, these bugs become trivial to debug.
Start with the Toolblip URL Encoder/Decoder. Paste your broken URL. Decode it fully. You'll see exactly what's happening.