Base64 Decode Shows Garbled Text — UTF-8 and Wrong Charset
Paste JWT payload into an online decoder — get {"name":"å±±ç°å¤ªéƒŽ"} instead of kanji. Paste API response into atob() — diamond question marks everywhere. r/learnprogramming threads blame “base64 is broken” when the real issue is charset pipeline or you decoded JPEG bytes as UTF-8.
atob()returns UTF-8 text — It returns a “binary string” where each char is a byte 0–255. Multi-byte UTF-8 sequences split across char codes → mojibake unless you useTextDecoder.- “Just use decodeURIComponent(escape(atob(s)))” — Legacy hack; works sometimes, fails on invalid surrogate pairs. Use
TextDecoderin modern browsers. - Assuming all base64 is text — Images, PDFs, protobuf — decoding as string will always look “garbled.”
- URL-safe base64 —
-and_variants need normalization before decode; padding=often dropped in JWTs.
Correct UTF-8 decode in browser
function base64ToUtf8(b64) {
const binary = atob(b64.replace(/-/g, "+").replace(/_/g, "/"));
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
Node.js:
Buffer.from(b64, "base64").toString("utf8");
Common mojibake patterns
| You see | Likely cause |
|---|---|
é instead of é | UTF-8 bytes interpreted as Latin-1 / Windows-1252 |
| “ | Replacement char — invalid UTF-8 sequence |
å±± style CJK garbage | Double-encoding or wrong decoder |
Random symbols + % | You decoded compressed/binary data |
Double UTF-8: Text was UTF-8, someone ran it through Latin-1 decoder, re-encoded — fix at source, not with another guess decode.
JWT / JSON payloads
JWT middle segment is base64url (no padding). After decode you should get valid JSON — if JSON.parse fails but string “looks almost English,” check:
- Padding — add
=until length % 4 === 0 - URL-safe chars — swap
-/_to+// - UTF-8 names in claims — use TextDecoder path above
Use jwt decode tool for header/payload split without running token through sketchy pastebins.
When it’s not encoding — bad input
- Truncated copy (Slack/email line wrap)
- Whitespace/newlines inside string — strip before decode
- Data URL prefix still attached —
data:image/png;base64,must be removed for raw decode - Hex or base64 confusion — different alphabets
Binary vs text decision tree
Decode base64 → bytes
├─ Magic bytes PNG/GIF/PDF? → binary file, don't stringify
├─ Valid UTF-8 text? → TextDecoder utf-8
└─ Still garbled → ask source for charset or raw bytes
Server-side gotcha
PHP base64_decode + echo without Content-Type: charset=utf-8 — browser guesses wrong charset. Python .decode('utf-8') vs .decode('latin-1') — pick explicitly.
Email and MIME layers
SMTP often wraps bodies in quoted-printable or base64 Content-Transfer-Encoding inside MIME multipart. Decoding the outer base64 blob gives you another layer (headers + nested parts) — not plain user text. Mail parser libraries handle this; raw atob on an .eml paste won’t.
Spreadsheet and CSV exports
Excel sometimes base64-embeds small images or stores UTF-16LE blobs. Pasting into a web decoder shows noise because the payload was never UTF-8 text. Export as CSV UTF-8 or use the app’s native export before debugging encoding.
Tool: encode/decode safely
The base64 tool handles encode/decode in-browser — useful for quick UTF-8 checks without shipping secrets to random websites.
TL;DR
Base64 is transport, not charset. Use bytes → TextDecoder for UTF-8. Garbled CJK is usually Latin-1 misread or double encoding. Binary payloads aren’t supposed to read as English.