Base64 Encoding Chinese Characters Produces Garbage? Root Cause and the Correct Way to Handle It
Many developers hit the same problem the first time they Base64-encode Chinese: after encoding and decoding, the Chinese turns into garbage. For example, you encode "你好世界" and decode it back, only to get a pile of characters like 㽠好.
This isn't a bug in the Base64 algorithm itself — it's character encoding at work. This article helps you thoroughly understand the root cause of base64 Chinese garbage and gives you the correct way to handle it. If you just need to encode or decode Chinese safely, our Base64 encoder/decoder defaults to UTF-8 for Unicode and also lets you switch to GBK or Latin-1.
Why Base64 Encoding Chinese Produces Garbage
Base64 is fundamentally a binary-to-text encoding scheme — it operates on bytes, not characters. The problem is in the "characters → bytes" step:
- In UTF-8, one Chinese character takes 3 bytes
- In GBK, one Chinese character takes 2 bytes
If you encode using UTF-8 to turn Chinese into a byte stream, then decode using GBK to restore it, the byte counts don't match — and you get garbage.
A typical mistake:
// Wrong: directly using btoa to encode Chinese
btoa("你好世界");
// Uncaught DOMException: The string to be encoded contains characters outside of the Latin1 range.
The browser's btoa() only supports Latin1 characters (U+0000 to U+00FF) — it throws on Chinese. Some people work around it with escape():
// It runs, but has hidden risks
btoa(escape("你好世界"));
// When decoding, you must pair it with unescape(), or you get garbage
atob(btoa(escape("你好世界"))); // you get escape-encoded text, not the original
unescape(atob(btoa(escape("你好世界")))); // only this restores it
This works, but escape() is deprecated, and if the other party doesn't know to unescape when you transmit cross-platform, the decoded result is garbage.
The Correct Way to Base64-Encode and Decode Chinese
Core principle: convert the string to bytes with an agreed charset, then Base64-encode; reverse with the same charset when decoding.
Method 1: Using TextEncoder / TextDecoder (Recommended)
function base64EncodeUnicode(str) {
const bytes = new TextEncoder().encode(str); // string → UTF-8 bytes
const binary = Array.from(bytes, b => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64DecodeUnicode(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes); // UTF-8 bytes → string
}
// Test
const encoded = base64EncodeUnicode("你好世界,Hello!");
console.log(encoded); // "5L2g5aW95LiW55WM77yMSGVsbG8h"
const decoded = base64DecodeUnicode(encoded);
console.log(decoded); // "你好世界,Hello!"
Method 2: In Node.js
// Encode
const encoded = Buffer.from("你好世界", "utf-8").toString("base64");
console.log(encoded); // "5L2g5aW95LiW55WM"
// Decode — note you must specify utf-8
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
console.log(decoded); // "你好世界"
// The root of garbage: encoding and decoding used different character sets
const wrongDecode = Buffer.from(encoded, "base64").toString("latin1");
console.log(wrongDecode); // "ä½ å¥½ä¸ç" ← this is the garbage you see
Method 3: Handling Existing GBK-Encoded Data
The same word 「你好」 yields different Base64 under UTF-8 vs GBK:
| Charset | Bytes (hex) | Base64 |
|---|---|---|
| UTF-8 | E4 BD A0 E5 A5 BD | 5L2g5aW9 |
| GBK | C4 E3 BA C3 | xOO6ww== |
Decode xOO6ww== as UTF-8 and you get runs of �; decode 5L2g5aW9 as Latin-1 and you get shapes like ä½ å¥½.
In the on-site tool: set charset to GBK, paste xOO6ww==, decode — you get 「你好」. In code:
const gbkBytes = Uint8Array.from(atob("xOO6ww=="), c => c.charCodeAt(0));
const text = new TextDecoder("gbk").decode(gbkBytes);
console.log(text); // "你好"
Encoding plain text as GBK-then-Base64 is also available via the same charset control (a reverse map built from TextDecoder('gbk'), without Node Buffer gbk labels).
Practical Tips to Avoid Garbage
-
Use UTF-8 end-to-end: From the database to the backend API to the frontend, use UTF-8 everywhere — this eliminates encoding mismatches at the root.
-
Label the encoding in transit: If you must handle multi-encoding data, explicitly mark the character set in the protocol or file header (e.g.
Content-Type: text/plain; charset=utf-8). -
Don't mix encoding functions:
btoa/atobonly handles Latin1 — for Chinese, always go through byte conversion with an explicit charset. -
Local processing is safer: Base64 encoding/decoding isn't encryption, but if the content contains sensitive information (user data, internal API parameters), prefer a local tool to avoid the data passing through a third-party server.
-
Read the mojibake shape before switching: Runs of
�often mean GBK bytes read as UTF-8; leadingä¸/ä½often means UTF-8 bytes read as Latin-1. The on-site tool surfaces a hint in those cases under the default UTF-8 decode instead of silently answering wrong.
If you just need to quickly encode or decode a piece of Chinese text, use our Base64 Encoder/Decoder — all computation happens locally in your browser, data is never uploaded, UTF-8 is the default, GBK / Latin-1 are selectable, and URL-safe Base64 without padding is accepted.
A short decision tree for “garbage after decode”
- Does raw
atobthrow? — Input may be URL-safe Base64 (-/_) or missing padding; normalize before charset work. - Do you see
�replacement characters? — Bytes were almost certainly not UTF-8; try GBK on the Base64 tool, or recover original bytes from upstream. - Do you see
Ã/ä¸mojibake? — UTF-8 bytes were likely decoded as Latin-1 (or Windows-1252) once; switch charset or undo the wrong decode before re-encoding. - Does Base64 length look “too short” for the Chinese length? — Producer may have used GBK (2 bytes/char) while you assumed UTF-8 (3 bytes/char).
Write the producer charset next to the field in API docs (contentBase64 + charset=utf-8). Ambiguous Base64-of-text without a charset label is how this class of bug survives code review.
FAQ
Why does 「你好」 have two different Base64 strings?
Because Base64 encodes bytes. Under UTF-8 「你好」 is 6 bytes; under GBK it is 4 — so the Base64 differs (5L2g5aW9 vs xOO6ww==). Decode with the same charset the producer used.
The tool defaults to UTF-8 — what about legacy GBK systems?
On the Base64 encoder/decoder, switch Charset to GBK before encode or decode. If you still don't get readable Chinese, check which column in the table above matches your string.
I see 䏿–‡ but switching to GBK still fails?
That usually means the string was already mis-decoded once, then Base64-encoded again as UTF-8. Try Latin-1 to recover the byte shape, then reinterpret as UTF-8, or go back upstream for the original bytes. See Base64 Chinese garbled.
Why does btoa('中文') throw?
btoa only accepts Latin-1. Encode Chinese via TextEncoder (or the tool with UTF-8) before Base64 — don't feed CJK into btoa directly.
Implementer's note
Browser TextEncoder only emits UTF-8. The on-site tool decodes GBK with TextDecoder('gbk') and encodes GBK by reverse-mapping that decoder (table built on first use), so the charset control works without a Node iconv dependency. After decode, runs of � or Latin-1-shaped 丅 trigger a UI hint to switch charset instead of silently answering wrong.
Provided by ToolVault. Related tools: Base64 Encode/Decode, URL Encode/Decode, JWT Decoder. Related reading: Base64 Chinese garbled, Base64 vs Base64URL. See the homepage for more developer tools.
Related Tools
Related Articles
PDF to JPG: How to Pick the Right Resolution (DPI Guide + 3 Ways)
A complete guide to converting PDF pages to JPG: how DPI maps to sharpness, two-page and batch conversion, three approaches compared (in-browser local, command line, desktop software), plus a quality settings cheat sheet.
PDF Too Large for Email? How to Choose the Right Compression Quality (3 Local Levels)
How PDF compression works: lossy rasterization vs. lossless structure optimization, which of the three quality levels fits which use, and how to compare sizes before and after — entirely in your browser.
How to Split a PDF and Extract Pages: Range Syntax, Common Cases, and a Local Workflow
A complete guide to splitting PDFs by page range: the 1-3,5 range syntax, page-by-page extraction, and split-then-merge workflows — all in your browser, nothing uploaded.