An API doc tells you to "send it Base64 encoded." You hit a wall of characters like 7JWI64WV7ZWY7IS47JqU in a log. You put a value in a query string and the server receives something slightly different.
All three are the same subject. This post sorts out what Base64 and URL encoding actually do, and where they quietly break, with results from actually running them.
1. Base64 hides nothing — it transports
Start with the most common misconception. Base64 is not encryption. There's no key and no secret.
What it does is carry arbitrary data safely through text-only channels — email bodies, JSON strings, HTML attributes. That's why the output uses only 64 characters: A-Z a-z 0-9 + / =. They were picked because they survive almost anywhere.
Reversing it takes nothing at all. One line in a browser console:
atob('SGVsbG8=') // 'Hello'
So Base64-encoding a password or token before storing or logging it provides zero protection. It's unreadable to you, identical to plaintext for a machine. The flip side: when you spot such a string in a log, inspecting it is just as easy.
2. 🔴 Why non-ASCII text inflates so much
Base64 grows the input: 3 bytes in become 4 characters out, about 33% more. But with Korean (and any multi-byte script) the effect feels far larger.
Actual measurements:
| Input | Characters | Bytes | Base64 length |
|---|---|---|---|
Hello |
5 | 5 | 8 |
안녕하세요 |
5 | 15 | 20 |
안녕하세요 제이입니다 |
11 | 31 | 44 |
Five characters each, yet 8 versus 20. The growth happens twice.
- In UTF-8 a Korean character is 3 bytes (ASCII letters and digits are 1)
- Base64 then multiplies those bytes by 4/3
So the rough rule is character count × 4. It's also why inlining an image as a data: URL makes the payload bigger than the original file.
If size matters where you're using Base64, budget for that growth. To check the byte count of your source text, the Character Counter will tell you.
3. Why btoa('안녕') throws
Developers meet this one first.
btoa('안녕하세요')
// ❌ InvalidCharacterError: Invalid character
btoa is a byte-oriented function. It treats each character as one byte (0–255), so anything above that range is simply rejected.
To encode non-ASCII text you have to expand it into UTF-8 bytes first.
btoa(unescape(encodeURIComponent('안녕하세요')))
// '7JWI64WV7ZWY7IS47JqU'
The Encoder/Decoder does exactly this internally, which is why non-ASCII input converts without error. Tools that skip the step either throw or hand back mojibake — if the same string gives different results in different tools, this difference is the likely reason.
4. 🔴 Dropping Base64 straight into a URL breaks it
This is the trap people hit most. Some of Base64's 64 characters mean something else in a URL.
'~~~???' → fn5+Pz8/
There's a + and a / in that output. Paste it into an address and here's what happens:
| Character | What a URL does with it |
|---|---|
+ |
Conventionally read as a space in query strings |
/ |
Taken as a path separator |
= |
Confused with the key/value separator (it's the trailing padding) |
+ is the nasty one. It doesn't raise an error — it silently becomes a space, so decoding either fails later or, worse, passes with a subtly different value.
Two fixes:
- URL-encode it once more —
+becomes%2B,/becomes%2F,=becomes%3D - Use Base64URL — the variant that swaps
+for-and/for_. JWTs use it
"The token works in a URL most of the time" almost always traces back here. It's intermittent because only inputs that happen to produce + or / break — short test strings rarely do, real data does.
5. Four traps in URL encoding
Since it's the same tool, here's URL encoding too. All measured.
① + is not a space (in the standard functions)
encodeURIComponent('hello world') // 'hello%20world'
decodeURIComponent('hello+world') // 'hello+world' ← not a space
A space becomes %20. Reading + as a space is a separate convention that belongs only to HTML form submission (application/x-www-form-urlencoded). So a form value run through a standard decoder keeps its +, while the server may be reading that same + as a space. Two sides reading one string differently.
② A whole URL and a single value need different functions
encodeURIComponent('https://a.kr/b?c=d') // 'https%3A%2F%2Fa.kr%2Fb%3Fc%3Dd'
encodeURI('https://a.kr/b?c=d') // 'https://a.kr/b?c=d' (unchanged)
encodeURIComponent is for wrapping one value, so it also escapes / ? : @ & = + $ # , ;. Apply it to a full address and the URL's own structure turns into characters. Go the other way — encodeURI on a parameter value — and an & inside the value survives, truncating your value into an extra parameter.
Passing a redirect address as a parameter is where this bites. There, encodeURIComponent on the value is correct.
③ Encode twice and % becomes %25
encodeURIComponent(encodeURIComponent('a b')) // 'a%2520b'
The % of %20 gets encoded again. Seeing %2520 or %25EC%25... on screen means encoding was applied twice somewhere — usually a framework already handled it and the code wrapped it once more. Decode only once and the value comes back half-unwrapped.
④ A string containing % fails to decode at all
decodeURIComponent('100%') // ❌ URIError: URI malformed
decodeURIComponent('50%25') // '50%'
If % isn't followed by two hex digits, the decoder throws. That's how text with a percent sign — a discount, a return rate — put into a URL unencoded takes a whole page down with a 500. It's also why raw user input should never be concatenated into an address.
6. When decoding works but the result is wrong
Base64 decoding is more forgiving than you'd expect. Measured:
| Input | Result |
|---|---|
SGVsbG8= |
Hello |
SGVsbG8 (no padding) |
Hello — passes anyway |
SGVs bG8= (inner space) |
Hello — whitespace ignored |
안녕 |
❌ error |
Missing padding (=) usually still decodes. So "it must be the padding" is generally the wrong guess. The real cause is elsewhere — classically the + that turned into a space earlier. Whitespace is ignored, so it passes without error and only the result changes.
Mojibake in the output means UTF-8 handling was skipped at encoding time; an outright error means non-Base64 characters got in.
7. Practical rules
- Don't reach for Base64 when you need security. Encoding is not protection
- Budget for the size. Non-ASCII text runs about character count × 4
- Always wrap Base64 once more, or use Base64URL, before putting it in a URL.
+/=are the problem encodeURIComponentfor a value,encodeURIfor a whole address%2520means double encoding. Find the one extra wrap and remove it
To check a value right now, try the Encoder/Decoder. It switches between Base64 and URL encoding in one screen, and when you flip the direction it hands the current output back as input — so you can peel a double-encoded value one layer at a time. Invalid input is flagged on the spot. Everything runs in your browser, so a token or response you paste never leaves your machine.
If what you decoded is JSON, continue in the JSON Formatter; to find where two values diverged, the Diff Checker is the neighboring tool.
Summary
- Base64 is not encryption — one line reverses it. Never use it to hide anything
- Non-ASCII text grows by UTF-8's 3 bytes × 4/3, roughly character count × 4
- 🔴 The
+/=in the output corrupt values inside URLs — encode once more or use Base64URL - Reading
+as a space is a form-submission convention only; standard decoders leave it alone %2520means double encoding, and text containing%fails to decode entirely