← Tất cả bài viết

URL encoding (percent-encoding) giải thích đầy đủ

19/07/2026

Bạn dán một đường link chứa dấu tiếng Việt vào trình duyệt và thấy nó biến thành một chuỗi %E1%BA%BF khó hiểu? Hay bạn gọi API với query param có khoảng trắng và nhận về lỗi 400? Gần như chắc chắn thủ phạm là URL encoding (còn gọi là percent-encoding). Đây là một trong những khái niệm nền tảng mà mọi backend developer đều gặp hằng ngày, nhưng lại rất hay bị hiểu lơ mơ dẫn đến bug khó lần. Bài này sẽ giải thích đầy đủ, chính xác kỹ thuật, kèm ví dụ code JavaScript và bảng tra cứu.

URL encoding là gì và vì sao cần nó?

URL (chính xác hơn là URI, theo RFC 3986) chỉ được phép chứa một tập ký tự ASCII rất hạn chế. Cụ thể, một URL "sạch" chỉ nên gồm chữ cái Latin không dấu (A-Z, a-z), chữ số (0-9), một vài ký tự đặc biệt an toàn, và các ký tự có ý nghĩa cấu trúc như /, ?, #.

Vậy còn tất cả những thứ khác thì sao — khoảng trắng, dấu tiếng Việt (, đ, ế), ký tự Unicode, hay chính những ký tự cấu trúc khi chúng cần xuất hiện như dữ liệu thay vì cú pháp? Chúng bắt buộc phải được mã hoá. Có vài lý do cốt lõi:

  • An toàn khi truyền: URL được nhúng vào HTTP header, HTML, email... nơi một số ký tự có thể bị hiểu sai hoặc bị cắt.
  • Tránh nhập nhằng cú pháp: nếu giá trị của bạn chứa & hay =, trình phân tích URL sẽ nhầm nó là dấu phân cách tham số.
  • Hỗ trợ mọi ký tự: khoảng trắng và Unicode không phải ASCII an toàn nên phải quy đổi về dạng ASCII.

Percent-encoding hoạt động như thế nào?

Cơ chế rất đơn giản và nhất quán: mỗi ký tự cần mã hoá được chuyển thành các byte của nó theo bảng mã UTF-8, rồi từng byte được viết dưới dạng %XX, trong đó XX là giá trị byte ở hệ thập lục phân (hexadecimal).

Với ký tự ASCII (một byte), kết quả là một cặp %XX duy nhất:

  • Khoảng trắng có mã ASCII là 32 = 0x20%20
  • @ có mã 64 = 0x40%40
  • # có mã 35 = 0x23%23

Với ký tự Unicode ngoài ASCII, UTF-8 dùng nhiều byte nên kết quả là nhiều cặp %XX liền nhau. Ví dụ chữ trong tiếng Việt được UTF-8 mã hoá thành 3 byte E1 BB AF, nên percent-encoding của nó là %E1%BB%AF. Đây chính là lý do một từ tiếng Việt ngắn lại "phình" ra thành chuỗi % dài trên URL.

Ký tự reserved vs unreserved (RFC 3986)

RFC 3986 chia ký tự thành hai nhóm chính:

Unreserved — không cần mã hoá

Gồm chữ cái, chữ số và bốn ký tự - _ . ~. Những ký tự này luôn an toàn, không bao giờ mang ý nghĩa cấu trúc, nên bạn không cần (và không nên) mã hoá chúng.

Reserved — có ý nghĩa cấu trúc

Gồm các ký tự như : / ? # [ ] @ ! $ & ' ( ) * + , ; =. Chúng đóng vai trò phân tách các thành phần của URL. Ví dụ:

  • / ngăn cách các đoạn đường dẫn (path segment).
  • ? đánh dấu bắt đầu query string.
  • & ngăn cách các cặp tham số trong query.
  • = ngăn cách tên và giá trị của một tham số.
  • # đánh dấu fragment (phần sau dấu thăng).

Điểm mấu chốt: khi một ký tự reserved xuất hiện trong GIÁ TRỊ dữ liệu (chứ không phải làm dấu phân cách), nó bắt buộc phải được mã hoá. Giả sử bạn muốn truyền giá trị tìm kiếm là chuỗi a=b&c:

// SAI: & và = bị hiểu là cú pháp
https://api.site/search?q=a=b&c

// ĐÚNG: mã hoá giá trị
https://api.site/search?q=a%3Db%26c

Ở ví dụ SAI, server sẽ đọc q=a và một tham số c rỗng — hoàn toàn không phải ý bạn muốn.

encodeURI vs encodeURIComponent trong JavaScript

Đây là phần quan trọng nhất và cũng hay gây bug nhất. JavaScript cung cấp hai hàm mã hoá, và chúng không thể thay thế cho nhau:

  • encodeURI() dùng để mã hoá toàn bộ một URL đã hoàn chỉnh. Nó giữ nguyên các ký tự reserved cấu trúc (: / ? # & =) vì nó giả định các ký tự đó đang làm nhiệm vụ cú pháp. Nó chỉ mã hoá những thứ chắc chắn không hợp lệ như khoảng trắng, dấu tiếng Việt.
  • encodeURIComponent() dùng để mã hoá một mảnh giá trị sẽ được ghép vào URL (tên/giá trị của query param, một path segment). Nó mã hoá cả các ký tự reserved như / ? & = #.

Nguyên tắc thực hành: khi ghép giá trị vào URL, hãy dùng encodeURIComponent cho từng mảnh. Chỉ dùng encodeURI khi bạn có sẵn cả một URL và chỉ muốn "làm sạch" các ký tự bất hợp lệ.

const query = "Xin chào & tạm biệt";

encodeURI(query);
// "Xin%20ch%C3%A0o%20&%20t%E1%BA%A1m%20bi%E1%BB%87t"
// -> Chú ý: dấu & KHÔNG bị mã hoá -> sẽ làm hỏng query!

encodeURIComponent(query);
// "Xin%20ch%C3%A0o%20%26%20t%E1%BA%A1m%20bi%E1%BB%87t"
// -> dấu & thành %26, an toàn để làm giá trị param

Cách xây URL đúng chuẩn khi có nhiều tham số:

const base = "https://api.chovn.site/search";
const params = new URLSearchParams({
  q: "a=b&c",
  lang: "tiếng việt",
});
const url = `${base}?${params.toString()}`;
// URLSearchParams tự động encode cho bạn -> an toàn tuyệt đối

Dùng URLSearchParams là cách được khuyến nghị nhất vì nó tự lo phần mã hoá, tránh việc ghép chuỗi thủ công dễ sai. Nếu muốn kiểm tra nhanh một chuỗi bất kỳ sẽ được mã hoá ra sao, bạn có thể thử ngay với công cụ URL Encode / Decode.

Cái bẫy "+" và khoảng trắng

Đây là một trong những nhầm lẫn kinh điển. Có hai quy ước mã hoá khoảng trắng song song tồn tại:

  • Theo chuẩn percent-encoding của URL, khoảng trắng là %20.
  • Theo định dạng application/x-www-form-urlencoded (dùng khi submit form HTML và trong query string truyền thống), khoảng trắng có thể được viết là dấu +.

Hệ quả: một dấu + trong query string có thể mang hai nghĩa khác nhau tùy cách server giải mã. Nếu bạn muốn truyền dấu cộng theo nghĩa đen (ví dụ số điện thoại +84 hay một biểu thức toán), bạn PHẢI mã hoá nó thành %2B, nếu không nó có thể bị hiểu thành khoảng trắng.

encodeURIComponent("+84 912"); // "%2B84%20912"  (đúng)

// Nếu để nguyên "+84 912" trên query,
// nhiều server sẽ decode thành "84 912" (mất dấu +)
// hoặc " 84 912" tùy cách xử lý

Lời khuyên: luôn dùng encodeURIComponent hoặc URLSearchParams để không phải tự đoán ký tự nào cần thoát.

Ứng dụng thực tế

  • Query param: mọi giá trị tìm kiếm, bộ lọc do người dùng nhập đều phải encode trước khi ghép vào URL.
  • Tiếng Việt trên URL: slug bài viết, từ khoá tìm kiếm tiếng Việt sẽ thành chuỗi %XX — trình duyệt hiện đại hiển thị lại chữ có dấu cho dễ đọc, nhưng bản chất truyền đi vẫn là percent-encoded.
  • Truyền token và redirect_uri (OAuth): đây là ca đặc biệt nhạy cảm. Tham số redirect_uri trong OAuth bản thân nó là một URL đầy đủ (có ://, ?, &). Khi nhét URL này vào làm giá trị của một query param khác, bạn BẮT BUỘC phải encode toàn bộ nó bằng encodeURIComponent, nếu không các ký tự & bên trong sẽ phá vỡ URL cha.
const redirectUri = "https://app.chovn.site/callback?next=/home";
const authUrl =
  "https://accounts.google.com/o/oauth2/v2/auth" +
  "?client_id=xxx" +
  "&response_type=code" +
  "&redirect_uri=" + encodeURIComponent(redirectUri);
// redirect_uri được encode -> dấu ? và & bên trong không phá URL cha

Bảng tra cứu nhanh ký tự → mã

Ký tựPercent-encodedGhi chú
(khoảng trắng)%20hoặc + trong form-urlencoded
!%21reserved
#%23đánh dấu fragment
$%24reserved
&%26phân tách param
+%2Btránh bị hiểu là khoảng trắng
/%2Fphân tách path
=%3Dngăn tên/giá trị
?%3Fbắt đầu query
@%40reserved
%E1%BB%AF3 byte UTF-8
đ%C4%912 byte UTF-8

Tóm tắt

URL encoding không phải là chuyện phức tạp một khi bạn nắm được nguyên tắc: URL chỉ chấp nhận một tập ASCII an toàn, mọi thứ khác được quy về byte UTF-8 và viết dưới dạng %XX. Hãy ghi nhớ ba điều cốt lõi:

  1. Ký tự reserved (/ ? & = #) phải được encode khi nằm trong giá trị, không phải làm cú pháp.
  2. Dùng encodeURIComponent (hoặc URLSearchParams) cho từng mảnh giá trị; chỉ dùng encodeURI cho URL hoàn chỉnh.
  3. Cẩn thận với + và khoảng trắng trong query string form-urlencoded.

Nắm vững những điều này sẽ giúp bạn tránh vô số bug khó chịu khi làm việc với API, OAuth và URL tiếng Việt. Muốn kiểm tra hoặc giải mã nhanh một chuỗi bất kỳ? Hãy dùng ngay công cụ miễn phí URL Encode / Decode của DevToolHub — dán vào, xem kết quả tức thì, không gửi dữ liệu lên server.