Base64 decode tiếng Việt bị lỗi dấu — nguyên nhân atob và cách sửa UTF-8
Base64 chỉ là lớp vận chuyển byte — không chọn charset giúp bạn. atob() trả về binary string Latin-1; chữ Việt (UTF-8 đa byte) sẽ lỗi dấu nếu đọc thẳng. Đường đúng: byte → TextDecoder('utf-8') (trình duyệt) hoặc Buffer.from(..., 'base64').toString('utf8') (Node). Thấy á / é → gần như chắc UTF-8 bị đọc như Latin-1. Kiểm tra nhanh trên công cụ Base64 Kawa (chạy local trên trình duyệt) trước khi đổ lỗi cho “Base64 hỏng”.
Bạn paste payload Base64 vào atob(), console in ra dạng Xin chà o thay vì Xin chào. JWT claim name thành Äà Nẵng thay vì Đà Nẵng. Trên Slack / Discord nhóm outsourcing Việt Nam, thread kiểu này lặp lại: “Base64 bị lỗi tiếng Việt?” — thực ra Base64 ổn; pipeline charset mới hỏng.
Bài này tập trung góc tiếng Việt + dấu + UTF-8 + atob: vì sao lỗi, code sửa, bảng nhận diện mojibake, JWT/Base64URL, và khi “lỗi” thật ra là bạn đang decode file nhị phân.
Ai nên đọc
- Frontend gọi API trả field Base64 chứa tên, địa chỉ, ghi chú tiếng Việt
- Dev đọc JWT / session token có claim Unicode
- Ai đang dùng
atob/btoavà thấy dấuă â ê ô ơ ư đbị vỡ - Người debug log email MIME hoặc Data URI rồi tưởng “decode sai encoding”
Hiện tượng: atob + tiếng Việt
Chuỗi UTF-8 "Xin chào" encode Base64 là WGluIGNow6Bv. Nếu bạn chỉ gọi atob rồi tin kết quả là Unicode:
const b64 = "WGluIGNow6Bv"; // "Xin chào" (UTF-8 → Base64)
console.log(atob(b64));
// Kết quả là binary string Latin-1 — khi in như text thường sẽ lỗi dấu
So với đường đúng (byte → UTF-8):
function base64ToUtf8(b64) {
const normalized = b64
.replace(/-/g, "+")
.replace(/_/g, "/")
.replace(/\s+/g, "");
const pad =
normalized.length % 4 === 0
? ""
: "=".repeat(4 - (normalized.length % 4));
const binary = atob(normalized + pad);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
console.log(base64ToUtf8("WGluIGNow6Bv"));
// → "Xin chào"
console.log(base64ToUtf8("xJDDoCBO4bq1bmcgwrcgVmnhu4d0IE5hbQ=="));
// → "Đà Nẵng · Việt Nam"
Lưu ý encode: btoa('Xin chào') cũng sai hoặc ném lỗi — btoa/atob không phải API UTF-8. Cặp encode/decode khớp:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
console.log(utf8ToBase64("Xin chào, Đà Nẵng!"));
console.log(base64ToUtf8(utf8ToBase64("Xin chào, Đà Nẵng!")));
// → "Xin chào, Đà Nẵng!"
Vì sao atob làm lỗi dấu?
atob được thiết kế thời Latin-1 / “binary string”: mỗi code unit = đúng 1 byte (0–255).
Tiếng Việt trong UTF-8 thường 2–3 byte / ký tự có dấu. Ví dụ chữ á là C3 A1. atob tách thành hai “ký tự” Latin-1 → khi in ra HTML/console như text Latin-1 bạn thấy á.
| atob giả định | Chữ Việt (UTF-8) |
|---|---|
| 1 ký tự hiển thị = 1 byte | 1 ký tự có dấu = nhiều byte |
| Byte được đọc thẳng thành char | Cần ghép byte rồi decode charset |
| Đủ cho ASCII thuần | Bắt buộc TextDecoder / Buffer UTF-8 |
Base64 không “biết” UTF-8 hay Windows-1258. Nó chỉ map 3 byte → 4 ký tự alphabet. Charset nằm ở lớp trước encode và sau decode.
Code đúng: trình duyệt và Node
Trình duyệt (khuyến nghị)
Dùng base64ToUtf8 ở trên — luôn strip whitespace, chuẩn hóa Base64URL, bổ sung padding.
Node.js
const decoded = Buffer.from("WGluIGNow6Bv", "base64").toString("utf8");
console.log(decoded); // "Xin chào"
Hack cũ cần tránh khi có thể
// Legacy — đôi khi chạy, dễ vỡ surrogate / invalid sequence
decodeURIComponent(escape(atob(b64)));
Ưu tiên TextDecoder. Chỉ giữ hack khi phải support môi trường cực cũ và đã có test với chuỗi tiếng Việt thật.
JWT dùng Base64URL: + → -, / → _, thường bỏ padding =. Gọi atob trực tiếp trên segment JWT dễ fail hoặc ra rác. Luôn chuẩn hóa trước (như hàm trên), rồi mới decode UTF-8.
Bảng nhận diện mojibake tiếng Việt
| Bạn thấy | Nguyên nhân hay gặp | Hướng xử lý |
|---|---|---|
á thay á, é thay é | UTF-8 bị đọc Latin-1 / Windows-1252 | Byte → TextDecoder utf-8 |
Chuỗi “gần đúng nhưng sai” kiểu Äá»… | Double-encoding hoặc decode sai lớp | Sửa ở nguồn encode; đừng đoán thêm 1 lần decode |
| Ký tự thay thế U+FFFD (diamond ?) | Sequence UTF-8 không hợp lệ / cắt giữa chừng | Kiểm tra truncate, copy thiếu ký tự |
Toàn ký tự lạ + % | Payload là binary (ảnh, zip) bị stringify | Đừng decode như text |
| ASCII ổn, chỉ dấu Việt sai | Charset pipeline; ASCII 1 byte nên “may mắn” đúng | Ép UTF-8 end-to-end |
Double UTF-8: text đã là UTF-8 → ai đó decode Latin-1 → encode lại Base64. Client decode “đúng UTF-8” vẫn ra rác. Khi đó phải sửa chỗ encode gốc, không phải thêm một vòng TextDecoder may rủi.
Case study 1: API trả tên khách hàng
Team nhận JSON:
{ "customerNameB64": "WHXDom4gVGjhu40gTWluaA==" }
(Xuân Thọ Minh dạng UTF-8 → Base64.)
Dev A:
el.textContent = atob(data.customerNameB64); // ❌ lỗi dấu
Dev B:
el.textContent = base64ToUtf8(data.customerNameB64); // ✅ "Xuân Thọ Minh"
Checklist PR nhỏ:
- Unit test với
"Nguyễn"(Tmd1eeG7hW4=),"Đỗ","Trần" - Không dùng
btoa/atobtrực tiếp trên Unicode - Document charset: UTF-8 trước Base64
Case study 2: JWT claim tiếng Việt
Segment payload JWT (giữa hai dấu .) sau chuẩn hóa + decode UTF-8 phải ra JSON hợp lệ:
{ "name": "Phạm Minh", "city": "Hà Nội" }
Nếu JSON.parse fail nhưng chuỗi “gần giống tiếng Anh/Việt”:
- Padding — thêm
=đến khilength % 4 === 0 - Đổi
-/_về+// - Dùng đường
TextDecoder, khôngatobrồi tin ngay
Đọc nhanh header/payload (không verify chữ ký) có thể dùng JWT decode — vẫn nhớ: decode ≠ xác thực token.
Khi không phải lỗi encoding
Cây quyết định sau decode Base64 → bytes:
bytes
├─ Magic PNG/JPEG/PDF/GIF? → file nhị phân — đừng in như UTF-8
├─ UTF-8 hợp lệ? → TextDecoder('utf-8')
└─ Vẫn rác → hỏi nguồn charset / raw bytes; nghi double-encode
Các lỗi input hay gặp:
- Copy thiếu (Slack wrap dòng, email cắt)
- Còn khoảng trắng / xuống dòng trong chuỗi —
replace(/\s+/g, "") - Dính prefix Data URI:
data:image/png;base64,— bỏ phần trước dấu phẩy - Nhầm hex với Base64
Server / HTTP
PHP base64_decode + echo thiếu Content-Type: ...; charset=utf-8 → trình duyệt đoán charset sai. Python cần .decode('utf-8') tường minh, đừng để default mơ hồ.
Email MIME
SMTP hay bọc body bằng Base64 bên trong multipart. Decode lớp ngoài có thể ra headers + phần lồng — không phải plain text người dùng. Dùng mail parser, không atob cả file .eml.
Quy trình debug 5 phút
- Xác định payload là text hay binary
- Chuẩn hóa Base64URL + padding + strip whitespace
- Decode bằng
TextDecoder/BufferUTF-8 - So với chuỗi vàng:
Xin chào,Đà Nẵng,Việt Nam - Vẫn sai → nghi double-encode hoặc charset nguồn ≠ UTF-8
- Kiểm tra nhanh trên Base64 tool Kawa (xử lý trên trình duyệt, không cần đăng ký) trước khi đổ lỗi API
🔄 Thử giải mã Base64 ngay tại đây
FAQ nhanh trong bài
Có nên dùng Base64 để “giấu” tiếng Việt / bảo mật không?
Không. Base64 ai cũng decode được. Đây là encoding vận chuyển, không phải mã hóa.
Windows-1258 / TCVN3 còn gặp không?
Hiếm trên API web mới (thường UTF-8), nhưng file cũ / Excel / hệ thống kế toán nội địa đôi khi còn. Nếu TextDecoder('utf-8') ra rác có hệ thống, hỏi nguồn charset gốc — đừng đoán mù.
Encode phía client thế nào cho khớp?
TextEncoder → bytes → btoa binary string (như utf8ToBase64 ở trên). Server và client phải cùng UTF-8.
Tóm tắt
| Việc cần làm | Tránh |
|---|---|
| Byte → UTF-8 qua TextDecoder / Buffer | Tin atob() là “chuỗi Unicode” |
| Chuẩn hóa Base64URL + padding | Paste JWT thô vào atob |
| Test với chữ có dấu Việt | Chỉ test ASCII "hello" |
| Phân biệt text vs binary | Stringify JPEG rồi hỏi “sao lỗi dấu” |
→ Thử encode/decode UTF-8 trên công cụ Base64 — chạy local, phù hợp paste log nội bộ.