Skip to content
Encoding2026-05-17Begin6 min read

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.

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:

CharsetBytes (hex)Base64
UTF-8E4 BD A0 E5 A5 BD5L2g5aW9
GBKC4 E3 BA C3xOO6ww==

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

  1. 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.

  2. 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).

  3. Don't mix encoding functions: btoa/atob only handles Latin1 — for Chinese, always go through byte conversion with an explicit charset.

  4. 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.

  5. 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”

  1. Does raw atob throw? — Input may be URL-safe Base64 (-/_) or missing padding; normalize before charset work.
  2. Do you see � replacement characters? — Bytes were almost certainly not UTF-8; try GBK on the Base64 tool, or recover original bytes from upstream.
  3. 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.
  4. 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.