URL Decode JavaScript — decodeURIComponent()
Sử dụng Giải mã URL Trực tuyến miễn phí trực tiếp trên trình duyệt — không cần cài đặt.
Dùng thử Giải mã URL Trực tuyến trực tuyến →Các chuỗi percent-encoded xuất hiện liên tục trong code JavaScript — một truy vấn tìm kiếm đến dưới dạng q=standing+desk%26price%3A200, một redirect OAuth dưới dạng next=https%3A%2F%2Fdashboard.internal%2F, một đường dẫn lưu trữ dưới dạng reports%2F2025%2Fq1.pdf. Cách URL decode trong JavaScript phụ thuộc vào việc chọn đúng một trong ba hàm tích hợp: decodeURIComponent(), decodeURI() và URLSearchParams — và sự lựa chọn giữa chúng là nguyên nhân gốc rễ của hầu hết các lỗi hỏng dữ liệu thầm lặng tôi đã thấy trong các codebase sản xuất, đặc biệt là edge case +-là-dấu-cách và double-decoding. Để giải mã nhanh mà không cần viết code, Bộ giải mã URL của ToolDeck xử lý ngay lập tức trong trình duyệt. Hướng dẫn giải mã URL JavaScript này bao gồm cả ba hàm một cách chi tiết (ES2015+ / Node.js 10+): khi nào dùng từng hàm, chúng khác nhau như thế nào đối với dấu cách và ký tự đặc biệt, giải mã từ file và HTTP request, xử lý lỗi an toàn, và bốn sai lầm gây ra các bug sản xuất tinh vi nhất.
- ✓decodeURIComponent() giải mã tất cả các chuỗi percent-encoded — đây là lựa chọn đúng cho các giá trị tham số query riêng lẻ và các đoạn đường dẫn
- ✓decodeURI() giữ nguyên các ký tự URI cấu trúc như / ? & = # : — chỉ dùng khi giải mã một chuỗi URL hoàn chỉnh, không bao giờ cho các giá trị riêng lẻ
- ✓URLSearchParams.get() trả về các giá trị đã được giải mã — gọi decodeURIComponent() thêm lên nó gây ra double-decoding
- ✓decodeURIComponent() KHÔNG giải mã + thành dấu cách — đối với dữ liệu form-encoded (application/x-www-form-urlencoded), dùng URLSearchParams hoặc thay thế + trước khi giải mã
- ✓Luôn bọc decodeURIComponent() trong try/catch khi input đến từ dữ liệu người dùng — ký tự % đơn lẻ hoặc chuỗi không đầy đủ sẽ ném URIError
URL Decoding là gì?
Percent-encoding (được định nghĩa chính thức trong RFC 3986) thay thế các ký tự không an toàn hoặc có ý nghĩa cấu trúc trong URL bằng dấu % theo sau là hai chữ số thập lục phân — giá trị byte UTF-8 của ký tự đó. URL decoding đảo ngược phép biến đổi này: mỗi chuỗi %XX được chuyển đổi trở lại thành byte gốc của nó, và chuỗi byte kết quả được diễn giải là văn bản UTF-8. Dấu cách được giải mã từ %20, dấu gạch chéo từ %2F, ký tự non-ASCII ü từ %C3%BC (biểu diễn UTF-8 hai byte của nó).
Các ký tự không bao giờ được mã hóa — gọi là ký tự không dành riêng — là chữ cái A–Z và a–z, chữ số 0–9, và - _ . ~. Tất cả những thứ khác hoặc có vai trò cấu trúc trong URL (như / phân tách các đoạn đường dẫn hoặc & phân tách các tham số query) hoặc phải được mã hóa khi dùng làm dữ liệu. Hệ quả thực tế: một bộ lọc tìm kiếm như status=active&tier=premium được mã hóa thành một giá trị tham số query duy nhất sẽ trông hoàn toàn khác với bản gốc.
// Percent-encoded — như nhận được trong HTTP request hoặc webhook payload "q=price%3A%5B200+TO+800%5D%20AND%20brand%3ANorthwood%20%26%20status%3Ain-stock"
// Sau khi URL decoding — bộ lọc truy vấn Elasticsearch gốc "q=price:[200 TO 800] AND brand:Northwood & status:in-stock"
decodeURIComponent() — Hàm Chuẩn để Giải Mã Giá Trị
decodeURIComponent() là công cụ chính của URL decoding trong JavaScript. Nó giải mã mọi chuỗi %XX trong chuỗi input — bao gồm các ký tự có ý nghĩa cấu trúc trong URL, như %2F (dấu gạch chéo), %3F (dấu hỏi), %26 (dấu và) và %3D (dấu bằng). Điều này làm cho nó là lựa chọn đúng để giải mã các giá trị tham số query riêng lẻ và các đoạn đường dẫn, nhưng là lựa chọn sai để giải mã một URL hoàn chỉnh — nơi các ký tự cấu trúc đó phải giữ nguyên mã hóa. Đây là hàm toàn cục: không cần import trong bất kỳ môi trường JavaScript nào.
Ví dụ tối giản hoạt động được
// Giải mã các giá trị tham số query riêng lẻ — trường hợp phổ biến
const city = decodeURIComponent('H%C3%A0%20N%E1%BB%99i') // 'Hà Nội'
const district = decodeURIComponent('Ho%C3%A0n%20Ki%E1%BA%BFm') // 'Hoàn Kiếm'
const category = decodeURIComponent('n%E1%BB%99i%20th%E1%BA%A5t%20v%C4%83n%20ph%C3%B2ng') // 'nội thất văn phòng'
const filter = decodeURIComponent('price%3A%5B200+TO+800%5D') // 'price:[200+TO+800]'
// Lưu ý: + KHÔNG được giải mã thành dấu cách — xem phần + trong bảng so sánh
console.log(city) // Hà Nội
console.log(district) // Hoàn Kiếm
console.log(category) // nội thất văn phòng
console.log(filter) // price:[200+TO+800]Giải mã URL redirect được trích xuất từ tham số query
// URL redirect đã được percent-encode khi nó được nhúng làm giá trị tham số
// phía nhận: trích xuất, sau đó sử dụng — không cần giải mã thủ công với URLSearchParams
const incomingUrl = 'https://auth.company.com/callback' +
'?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform' +
'&session_id=sid_7x9p2k'
const url = new URL(incomingUrl)
const rawNext = url.searchParams.get('next') // Tự động giải mã bởi URLSearchParams
const sessionId = url.searchParams.get('session_id') // 'sid_7x9p2k'
// rawNext đã được giải mã: 'https://dashboard.internal/reports?view=weekly&team=platform'
// ĐỪNG gọi decodeURIComponent(rawNext) thêm lần nữa — đó sẽ là double-decoding
const nextUrl = new URL(rawNext!)
console.log(nextUrl.hostname) // dashboard.internal
console.log(nextUrl.searchParams.get('view')) // weekly
console.log(nextUrl.searchParams.get('team')) // platformGiải mã các đoạn đường dẫn non-ASCII và Unicode
// REST API với các đoạn đường dẫn được quốc tế hóa
// Mỗi byte UTF-8 của ký tự gốc được percent-encode riêng biệt
const encodedSegments = [
'%E6%9D%B1%E4%BA%AC', // 東京 (Tokyo) — 3 byte mỗi ký tự
'M%C3%BCnchen', // München — ü được mã hóa thành 2 byte
'caf%C3%A9', // café — é dạng precomposed NFC
'H%C3%A0%20N%E1%BB%99i', // Hà Nội
]
encodedSegments.forEach(seg => {
console.log(decodeURIComponent(seg))
})
// 東京
// München
// café
// Hà Nội
// Trích xuất file key từ URL của storage API
// Object key chứa / nên được mã hóa thành %2F bên trong đoạn đường dẫn
const storageUrl = 'https://storage.api.example.com/v1/objects/reports%2F2025%2Fq1-financials.pdf'
const rawKey = new URL(storageUrl).pathname.replace('/v1/objects/', '')
// .pathname giải mã encoding ở cấp URL nhưng %2F (như %252F ở cấp URL) vẫn giữ
// Dùng decodeURIComponent cho bước cuối:
const fileKey = decodeURIComponent(rawKey) // 'reports/2025/q1-financials.pdf'
console.log(fileKey)decodeURIComponent() ném URIError khi input chứa % không theo sau là hai chữ số thập lục phân hợp lệ — ví dụ % đơn lẻ ở cuối chuỗi hoặc chuỗi như %GH. Luôn bọc nó trong try/catch khi giải mã input do người dùng cung cấp. Các pattern giải mã an toàn được đề cập trong phần Xử lý Lỗi bên dưới.Các Hàm URL Decoding JavaScript — Tham Chiếu Ký Tự
Ba hàm giải mã tích hợp khác nhau ở chỗ chúng giải mã chính xác những chuỗi nào. Bảng hiển thị hành vi cho các ký tự quan trọng nhất trong thực tế:
| Được mã hóa | Ký tự | decodeURIComponent() | decodeURI() | URLSearchParams |
|---|---|---|---|---|
| %20 | dấu cách | dấu cách ✅ | dấu cách ✅ | dấu cách ✅ |
| + | plus (form) | + (giữ nguyên) | + (giữ nguyên) | dấu cách ✅ |
| %2B | + literal | + ✅ | + ✅ | + ✅ |
| %26 | & | & ✅ | & (giữ nguyên) ❌ | & ✅ |
| %3D | = | = ✅ | = (giữ nguyên) ❌ | = ✅ |
| %3F | ? | ? ✅ | ? (giữ nguyên) ❌ | ? ✅ |
| %23 | # | # ✅ | # (giữ nguyên) ❌ | # ✅ |
| %2F | / | / ✅ | / (giữ nguyên) ❌ | / ✅ |
| %3A | : | : ✅ | : (giữ nguyên) ❌ | : ✅ |
| %40 | @ | @ ✅ | @ (giữ nguyên) ❌ | @ ✅ |
| %25 | % | % ✅ | % ✅ | % ✅ |
| %C3%BC | ü | ü ✅ | ü ✅ | ü ✅ |
Hai hàng quan trọng là + và các ký tự cấu trúc (%26, %3D, %3F). URLSearchParams giải mã + thành dấu cách vì nó tuân theo đặc tả application/x-www-form-urlencoded — đúng cho các lần gửi form HTML, nhưng khác với những gì decodeURIComponent() làm với cùng ký tự đó. Và decodeURI() lặng lẽ bỏ qua %26, %3D và %3F — trông có vẻ đúng cho đến khi một giá trị thực sự chứa dấu và hoặc dấu bằng đã được mã hóa.
decodeURI() — Giải Mã URL Hoàn Chỉnh Mà Không Phá Vỡ Cấu Trúc
decodeURI() là đối tác của encodeURI(). Nó giải mã một chuỗi URL hoàn chỉnh trong khi giữ nguyên các ký tự có ý nghĩa cấu trúc trong URI: ; , / ? : @ & = + $ #. Các ký tự này được giữ ở dạng percent-encoded (hoặc là ký tự literal nếu chúng xuất hiện không mã hóa trong input). Điều này làm cho decodeURI() an toàn để làm sạch các URL đầy đủ có thể chứa ký tự non-ASCII trong đường dẫn hoặc tên máy chủ, mà không vô tình làm sụp đổ cấu trúc query string.
Làm sạch URL với các đoạn đường dẫn non-ASCII
// URL CDN với các đoạn đường dẫn được quốc tế hóa và query string có cấu trúc // decodeURI() giải mã đường dẫn non-ASCII nhưng giữ nguyên ? & = const encodedUrl = 'https://cdn.example.com/assets/%E6%9D%B1%E4%BA%AC%2F2025%2Fq1-report.pdf' + '?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600' const readable = decodeURI(encodedUrl) console.log(readable) // https://cdn.example.com/assets/東京/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600 // ↑ Non-ASCII đã giải mã; ? & = được giữ nguyên — URL vẫn hợp lệ về mặt cấu trúc // decodeURIComponent sẽ phá hủy URL — : / ? & = tất cả được giải mã cùng lúc const broken = decodeURIComponent(encodedUrl) // Trông giống ở đây, nhưng URL như 'https%3A%2F%2F...' sẽ bị phá hủy
URL thay vì decodeURI() khi bạn cũng cần truy cập các thành phần URL riêng lẻ. new URL(str) chuẩn hóa input, xác nhận cấu trúc của nó và cung cấp .pathname, .searchParams và .hostname dưới dạng các thuộc tính đã được giải mã. Để dành decodeURI() cho các trường hợp bạn chỉ cần kết quả chuỗi và không thể dùng hàm khởi tạo URL (ví dụ: trong các môi trường Node.js 6 rất cũ không có class URL toàn cục).URLSearchParams — Giải Mã Tự Động cho Query Strings
URLSearchParams là cách thành ngữ để phân tích query strings trong JavaScript hiện đại. Mọi giá trị được trả về bởi .get(), .getAll() hoặc vòng lặp đều được giải mã tự động — bao gồm + thành dấu cách cho dữ liệu form-encoded. Nó có sẵn toàn cục trong tất cả các trình duyệt hiện đại và Node.js 10+, không cần import, và xử lý các edge case mà việc tách chuỗi thủ công thường làm sai.
Phân tích query string đến
// Phân tích query string callback webhook hoặc redirect OAuth
const rawSearch =
'?event_id=evt_9c2f4a1b' +
'&product_name=Standing+Desk+Pro' +
'&filter=price%3A%5B200+TO+800%5D' +
'&tag=ergonomic&tag=adjustable' +
'&redirect=https%3A%2F%2Fdashboard.internal%2Forders%3Fview%3Dpending'
const params = new URLSearchParams(rawSearch)
console.log(params.get('event_id')) // 'evt_9c2f4a1b'
console.log(params.get('product_name')) // 'Standing Desk Pro' ← + được giải mã thành dấu cách
console.log(params.get('filter')) // 'price:[200+TO+800]' ← %3A và %5B được giải mã
console.log(params.getAll('tag')) // ['ergonomic', 'adjustable']
console.log(params.get('redirect')) // 'https://dashboard.internal/orders?view=pending'
// Lặp qua tất cả các tham số
for (const [key, value] of params) {
console.log(`${key}: ${value}`)
}
// event_id: evt_9c2f4a1b
// product_name: Standing Desk Pro
// filter: price:[200+TO+800]
// tag: ergonomic
// tag: adjustable
// redirect: https://dashboard.internal/orders?view=pendingPhân tích query parameters từ URL trình duyệt hiện tại
// URL hiện tại: https://app.example.com/search
// ?q=standing+desk
// &category=office+furniture
// &sort=price_asc
// &page=2
interface SearchFilters {
query: string | null
category: string | null
sort: string
page: number
}
function getSearchFilters(): SearchFilters {
const params = new URLSearchParams(window.location.search)
return {
query: params.get('q'), // 'standing desk'
category: params.get('category'), // 'office furniture'
sort: params.get('sort') ?? 'relevance',
page: Number(params.get('page') ?? '1'),
}
}
const filters = getSearchFilters()
console.log(filters.query) // standing desk (+ được giải mã tự động)
console.log(filters.category) // office furnitureGiải Mã Dữ Liệu URL-Encoded từ File và Phản Hồi API
Hai tình huống xuất hiện liên tục trong các dự án thực tế: xử lý một file trên đĩa chứa dữ liệu percent-encoded (access logs, xuất dữ liệu, file lưu trữ webhook) và phân tích URL của HTTP request đến trong server Node.js. Cả hai đều theo cùng một nguyên tắc — dùng hàm khởi tạo URL hoặc URLSearchParams thay vì tách chuỗi thủ công — nhưng chi tiết thì khác nhau.
Đọc và giải mã các bản ghi URL-encoded từ file
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
// File: orders-export.txt — một bản ghi URL-encoded mỗi dòng
// customer_name=Nguy%E1%BB%85n+V%C4%83n+An&order_id=ord_9c2f4a&product=Standing+Desk+Pro&total=3750000%20VND
// customer_name=Tr%E1%BA%A7n+Th%E1%BB%8B+Mai&order_id=ord_7b3a1c&product=Ergonomic+Chair&total=2250000%20VND
// customer_name=L%C3%AA+Minh+Tu%E1%BA%A5n&order_id=ord_2e8d5f&product=Monitor+Arm&total=1350000%20VND
interface Order {
customerName: string | null
orderId: string | null
product: string | null
total: string | null
}
async function parseOrdersFile(filePath: string): Promise<Order[]> {
const fileStream = createReadStream(filePath, { encoding: 'utf-8' })
const rl = createInterface({ input: fileStream, crlfDelay: Infinity })
const orders: Order[] = []
for await (const line of rl) {
if (!line.trim()) continue
// URLSearchParams giải mã + thành dấu cách và các chuỗi %XX tự động
const params = new URLSearchParams(line)
orders.push({
customerName: params.get('customer_name'), // 'Nguyễn Văn An'
orderId: params.get('order_id'), // 'ord_9c2f4a'
product: params.get('product'), // 'Standing Desk Pro'
total: params.get('total'), // '3750000 VND'
})
}
return orders
}
const orders = await parseOrdersFile('./orders-export.txt')
console.log(orders[0])
// { customerName: 'Nguyễn Văn An', orderId: 'ord_9c2f4a', product: 'Standing Desk Pro', total: '3750000 VND' }Phân tích access log Nginx để giải mã các truy vấn tìm kiếm
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
// Các dòng access log Nginx trông như thế này:
// 192.168.1.42 - - [11/Mar/2026:10:23:01 +0000] "GET /api/search?q=standing%20desk%26brand%3ANorthwood&sort=price_asc HTTP/1.1" 200 1842
async function extractSearchQueries(logFile: string): Promise<string[]> {
const rl = createInterface({ input: createReadStream(logFile), crlfDelay: Infinity })
const queries: string[] = []
for await (const line of rl) {
// Trích xuất đường dẫn request từ dòng log
const match = line.match(/"GET ([^ ]+) HTTP/)
if (!match) continue
try {
const requestUrl = new URL(match[1], 'http://localhost')
const query = requestUrl.searchParams.get('q')
if (query) queries.push(query) // URLSearchParams giải mã tự động
} catch {
// Bỏ qua các dòng không hợp lệ — access logs có thể chứa các mục bị cắt
}
}
return queries
}
const queries = await extractSearchQueries('/var/log/nginx/access.log')
console.log(queries)
// ['standing desk&brand:Northwood', 'ergonomic chair', 'monitor arm 27 inch']Phân tích query parameters trong HTTP server Node.js
import http from 'http'
// URL đến: /api/products?q=standing+desk&warehouse=ha-noi&minStock=10&cursor=eyJpZCI6MTIzfQ%3D%3D
const server = http.createServer((req, res) => {
// Tạo URL đầy đủ — đối số thứ hai là base bắt buộc bởi hàm khởi tạo URL
const requestUrl = new URL(req.url!, 'http://localhost')
const searchQuery = requestUrl.searchParams.get('q') // 'standing desk'
const warehouseId = requestUrl.searchParams.get('warehouse') // 'ha-noi'
const minStock = Number(requestUrl.searchParams.get('minStock') ?? '0')
const cursor = requestUrl.searchParams.get('cursor') // 'eyJpZCI6MTIzfQ=='
if (!warehouseId) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'tham số warehouse là bắt buộc' }))
return
}
const cursorData = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ searchQuery, warehouseId, minStock, cursorData }))
})
server.listen(3000)Khi bạn cần kiểm tra một URL đã được mã hóa trong quá trình phát triển — để hiểu webhook đang gửi gì trước khi viết code phân tích — hãy dán trực tiếp vào Bộ giải mã URL của ToolDeck để xem dạng đã giải mã ngay lập tức mà không cần chạy script.
Giải Mã URL Qua Dòng Lệnh
Đối với các shell script, CI pipeline hoặc kiểm tra nhanh một lần các chuỗi đã mã hóa, một số cách tiếp cận hoạt động mà không cần viết script đầy đủ. Các one-liner Node.js là cross-platform; trên macOS và Linux, python3 cũng luôn có sẵn.
# ── Node.js one-liners ──────────────────────────────────────────────────
# Giải mã một giá trị percent-encoded
node -e "console.log(decodeURIComponent(process.argv[1]))" "H%C3%A0%20N%E1%BB%99i%20%26%20TP.%20H%E1%BB%93%20Ch%C3%AD%20Minh"
# Hà Nội & TP. Hồ Chí Minh
# Phân tích query string và in mỗi cặp key=value (đã giải mã)
node -e "
const params = new URLSearchParams(process.argv[1])
for (const [k, v] of params) console.log(`${k} = ${v}`)
" "q=standing+desk&warehouse=da-nang&minStock=10"
# q = standing desk
# warehouse = da-nang
# minStock = 10
# Giải mã và in đẹp body JSON được URL-encode (phổ biến khi debug webhook)
node -e "
const raw = decodeURIComponent(process.argv[1])
console.log(JSON.stringify(JSON.parse(raw), null, 2))
" '%7B%22event%22%3A%22purchase%22%2C%22amount%22%3A149.99%2C%22currency%22%3A%22VND%22%7D'
# {
# "event": "purchase",
# "amount": 149.99,
# "currency": "VND"
# }
# ── Python one-liner (có trên hầu hết các hệ thống macOS/Linux) ─────────────
# unquote_plus cũng giải mã + thành dấu cách — đúng cho dữ liệu form-encoded
python3 -c "
from urllib.parse import unquote_plus
import sys
print(unquote_plus(sys.argv[1]))
" "Standing+Desk+%26+Ergonomic+Chair"
# Standing Desk & Ergonomic Chair
# ── curl — ghi log và giải mã URL redirect từ response header ──────────
curl -sI "https://api.example.com/short/abc123" | grep -i location | node -e "
const line = require('fs').readFileSync('/dev/stdin', 'utf8')
const url = line.replace(/^location:s*/i, '').trim()
console.log(decodeURIComponent(url))
"Giải Mã Uyển Chuyển với decode-uri-component
Hàm tích hợp decodeURIComponent() ném URIError trên bất kỳ chuỗi không hợp lệ nào — bao gồm % ở cuối mà không có hai chữ số hex theo sau. Trong code sản xuất xử lý các URL do người dùng hoặc bên thứ ba cung cấp — log truy vấn tìm kiếm, URL nhấp từ chiến dịch email, dữ liệu web được scrape — các chuỗi không hợp lệ đủ phổ biến để gây ra crash. Gói decode-uri-component (~30M lượt tải npm hàng tuần) xử lý các chuỗi không hợp lệ một cách uyển chuyển bằng cách trả về chuỗi gốc không thay đổi thay vì ném lỗi.
npm install decode-uri-component # hoặc pnpm add decode-uri-component
import decodeUriComponent from 'decode-uri-component'
// Hàm native ném trên input không hợp lệ — có thể làm crash request handler
try {
decodeURIComponent('product%name%') // ❌ URIError: URI malformed
} catch (e) {
console.error('Native đã ném:', (e as Error).message)
}
// decode-uri-component trả về chuỗi raw thay vì ném lỗi
console.log(decodeUriComponent('product%name%'))
// product%name% ← các chuỗi không hợp lệ được giữ nguyên, không ném lỗi
// Các chuỗi hợp lệ được giải mã chính xác
console.log(decodeUriComponent('H%C3%A0%20N%E1%BB%99i%20%26%20%C4%90%C3%A0%20N%E1%BA%B5ng'))
// Hà Nội & Đà Nẵng
// Hỗn hợp: chuỗi hợp lệ được giải mã, chuỗi không hợp lệ được giữ nguyên
console.log(decodeUriComponent('TP.%20H%E1%BB%93%20Ch%C3%AD%20Minh%20%ZZ%20Region'))
// TP. Hồ Chí Minh %ZZ Region ← %ZZ không phải hex hợp lệ — giữ nguyên
// Sử dụng thực tế trong pipeline xử lý log
const rawSearchQueries = [
'standing%20desk%20ergonomic',
'price%3A%5B200+TO+800%5D',
'50%25+off', // ← sẽ ném với decodeURIComponent (bare %)
'wireless%20keyboard%20%26%20mouse',
]
const decoded = rawSearchQueries.map(q => decodeUriComponent(q))
console.log(decoded)
// ['standing desk ergonomic', 'price:[200+TO+800]', '50%25+off', 'wireless keyboard & mouse']Dùng decodeURIComponent() với try/catch cho code ứng dụng khi bạn muốn biết ngay khi nhận được input bất ngờ. Dùng decode-uri-component trong các data pipeline, bộ xử lý log và webhook handler khi bạn muốn tiếp tục xử lý ngay cả khi một số input chứa encoding không hợp lệ.
Xử Lý Các Chuỗi Percent-Encoded Không Hợp Lệ
Ký tự % đơn lẻ, chuỗi không đầy đủ như %A hoặc cặp không hợp lệ như %GH đều khiến decodeURIComponent() ném URIError: URI malformed. Bất kỳ input nào do người dùng kiểm soát — truy vấn tìm kiếm, URL fragment, các trường form chứa URL, tham số từ các liên kết chiến dịch email — đều có thể chứa các chuỗi này. Wrapper an toàn là cần thiết cho bất kỳ code nào đối mặt với bên ngoài.
Các wrapper giải mã an toàn cho các tình huống phổ biến
// ── 1. Giải mã an toàn cơ bản — trả về chuỗi gốc khi có lỗi ─────────
function safeDecode(encoded: string): string {
try {
return decodeURIComponent(encoded)
} catch {
return encoded // trả về raw input nếu giải mã thất bại — không bao giờ crash
}
}
// ── 2. Giải mã an toàn + xử lý + thành dấu cách (cho giá trị form-encoded) ─────────
function safeFormDecode(formEncoded: string): string {
try {
return decodeURIComponent(formEncoded.replace(/+/g, ' '))
} catch {
return formEncoded.replace(/+/g, ' ') // ít nhất thay + kể cả khi phần còn lại thất bại
}
}
// ── 3. Parser query string an toàn → plain object ──────────────────────
function parseQueryString(queryString: string): Record<string, string> {
const result: Record<string, string> = {}
try {
const params = new URLSearchParams(queryString)
for (const [key, value] of params) {
result[key] = value // URLSearchParams xử lý tất cả decoding bên trong
}
} catch {
// Trả về rỗng khi input hoàn toàn không hợp lệ
}
return result
}
// Cách dùng
console.log(safeDecode('H%C3%A0%20N%E1%BB%99i')) // Hà Nội
console.log(safeDecode('search%20for%2050%25+off')) // search for 50% off (bare %)
// → thực ra ổn; % ở đây là %25
console.log(safeDecode('malformed%string%')) // malformed%string% (không ném)
console.log(safeFormDecode('standing+desk+pro')) // standing desk pro
console.log(parseQueryString('q=hello+world&tag=node%20js&page=2'))
// { q: 'hello world', tag: 'node js', page: '2' }Các Lỗi Thường Gặp
Tôi đã thấy bốn pattern này gây ra hỏng dữ liệu thầm lặng hoặc crash bất ngờ trong sản xuất — thường chỉ khi một giá trị tình cờ chứa ký tự đặc biệt, có nghĩa là chúng lọt qua unit test và xuất hiện với dữ liệu người dùng thực.
Lỗi 1 — Double-decode một giá trị URLSearchParams
Vấn đề: URLSearchParams.get() trả về một chuỗi đã được giải mã. Gọi decodeURIComponent() thêm lên nó double-decode giá trị — chuyển bất kỳ % còn lại trong output đã giải mã thành %25, làm hỏng dữ liệu. Cách sửa: dùng giá trị từ URLSearchParams.get() trực tiếp — không cần giải mã thêm.
// ❌ URLSearchParams đã giải mã — giải mã lại làm hỏng các giá trị có %
const qs = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rawRate = qs.get('rate') // '50%' ← đã được giải mã
const wrongRate = decodeURIComponent(rawRate)
// '50%25' ← % được giải mã thành %25 ở lần thứ hai — lại sai// ✅ Dùng URLSearchParams.get() trực tiếp — giải mã là tự động
const qs = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rate = qs.get('rate') // '50%' ← đúng, không cần bước thêm
const redirect = qs.get('redirect') // 'https://dashboard.internal/'
const parsed = new URL(redirect!) // An toàn để khởi tạo — đã được giải mã
console.log(parsed.hostname) // dashboard.internalLỗi 2 — Không giải mã + thành dấu cách cho dữ liệu form-encoded
Vấn đề: Các lần gửi form và một số thư viện OAuth mã hóa dấu cách thành + (application/x-www-form-urlencoded). Gọi decodeURIComponent() trên dữ liệu này giữ + là dấu cộng literal. Cách sửa: dùng URLSearchParams để phân tích dữ liệu form-encoded hoặc thay thế + bằng dấu cách trước khi gọi decodeURIComponent().
// ❌ decodeURIComponent coi + là dấu cộng literal, không phải dấu cách
// OAuth token endpoint gửi: grant_type=authorization_code&code=SplxlOBeZQQYb...
// &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
// Một số triển khai OAuth cũ cũng dùng + cho dấu cách trong các code
const formBody = 'customer_name=Nguy%E1%BB%85n+V%C4%83n+An&product=Standing+Desk+Pro&qty=2'
const [, rawVal] = formBody.split('&')[0].split('=')
const name = decodeURIComponent(rawVal)
console.log(name) // 'Nguyễn+Văn+An' ← + không được chuyển thành dấu cách// ✅ Dùng URLSearchParams — tuân theo đặc tả application/x-www-form-urlencoded
const formBody = 'customer_name=Nguy%E1%BB%85n+V%C4%83n+An&product=Standing+Desk+Pro&qty=2'
const params = new URLSearchParams(formBody)
console.log(params.get('customer_name')) // 'Nguyễn Văn An' ← + được giải mã đúng thành dấu cách
console.log(params.get('product')) // 'Standing Desk Pro'Lỗi 3 — Dùng decodeURI() cho các giá trị tham số query riêng lẻ
Vấn đề: decodeURI() không giải mã %26 (&), %3D (=) hoặc %3F (?) — các ký tự thường được mã hóa bên trong các giá trị tham số. Dùng nó để giải mã một giá trị đơn lẻ sẽ để các chuỗi đó nguyên vẹn, tạo ra output không chính xác một cách thầm lặng. Cách sửa: dùng decodeURIComponent() cho các giá trị riêng lẻ; để dành decodeURI() cho các chuỗi URL đầy đủ.
// ❌ decodeURI không giải mã & và = — giá trị vẫn bị hỏng const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' const filter = decodeURI(encodedFilter) console.log(filter) // 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' ← %3D và %26 không được giải mã
// ✅ decodeURIComponent giải mã tất cả chuỗi bao gồm & và = const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' const filter = decodeURIComponent(encodedFilter) console.log(filter) // 'status=active&tier=premium®ion=eu-west' ← được giải mã chính xác
Lỗi 4 — Không bọc decodeURIComponent trong try/catch cho input người dùng
Vấn đề: Ký tự %đơn lẻ không theo sau là hai chữ số hex — phổ biến trong các truy vấn tìm kiếm người dùng gõ (“50% off”, “100% cotton”) — khiến decodeURIComponent() ném URIError: URI malformed, làm crash request handler nếu không được bắt. Cách sửa: luôn bọc trong try/catch khi input đến từ dữ liệu người dùng, URL fragment hoặc các hệ thống bên ngoài.
// ❌ Người dùng gõ '100% cotton' vào hộp tìm kiếm — bare % làm crash server
// GET /api/search?q=100%25+cotton ← Trường hợp cụ thể này ổn (%25 = %)
// GET /api/search?q=100%+cotton ← Trường hợp này crash (% không theo sau 2 chữ số hex)
app.get('/api/search', (req, res) => {
const query = decodeURIComponent(req.query.q as string)
// ↑ ném URIError: URI malformed cho '100% cotton' → lỗi 500 không được xử lý
})// ✅ Bọc trong try/catch — fallback về raw input nếu giải mã thất bại
app.get('/api/search', (req, res) => {
let query: string
try {
query = decodeURIComponent(req.query.q as string)
} catch {
query = req.query.q as string // dùng raw value thay vì crash
}
// tiếp tục xử lý an toàn — query đã được giải mã hoặc là raw
})decodeURIComponent vs decodeURI vs URLSearchParams — So Sánh Nhanh
| Phương thức | Giải mã %XX | Giải mã + thành dấu cách | Ném khi không hợp lệ | Giải mã & = ? # | Trường hợp sử dụng | Cần cài đặt |
|---|---|---|---|---|---|---|
| decodeURIComponent() | ✅ tất cả | ❌ không | ✅ URIError | ✅ có | Các giá trị riêng lẻ và đoạn đường dẫn | No |
| decodeURI() | ✅ hầu hết | ❌ không | ✅ URIError | ❌ không | Chuỗi URL hoàn chỉnh | No |
| URLSearchParams | ✅ tất cả | ✅ có | ❌ im lặng | ✅ có | Phân tích query string với giải mã tự động | No |
| Hàm khởi tạo URL | ✅ tất cả | ✅ có | ✅ TypeError | ✅ có | Phân tích và chuẩn hóa URL đầy đủ | No |
| decode-uri-component | ✅ tất cả | ❌ không | ❌ im lặng | ✅ có | Giải mã hàng loạt chịu đựng input không hợp lệ | npm install |
| querystring.unescape() | ✅ tất cả | ❌ không | ❌ im lặng | ✅ có | Node.js cũ (deprecated trong v16) | No (tích hợp sẵn) |
Đối với phần lớn các trường hợp, sự lựa chọn rút gọn thành ba tình huống. Dùng URLSearchParams để phân tích query string — nó xử lý decoding, quy tắc +-là-dấu-cách và các khóa lặp lại tự động. Dùng decodeURIComponent() (bọc trong try/catch) cho một giá trị đơn lẻ hoặc đoạn đường dẫn, đặc biệt khi bạn mong đợi các dấu gạch chéo được mã hóa %2F bên trong một đoạn. Dùng decodeURI() chỉ khi bạn có chuỗi URL đầy đủ với các ký tự non-ASCII trong đường dẫn và cần các ký tự cấu trúc (/ ? & =) giữ nguyên mã hóa trong output.
Các Câu Hỏi Thường Gặp
Công Cụ Liên Quan
Để giải mã chỉ với một cú nhấp mà không cần viết code, hãy dán chuỗi percent-encoded của bạn trực tiếp vào Bộ giải mã URL của ToolDeck — nó giải mã ngay lập tức trong trình duyệt, với kết quả sẵn sàng để sao chép vào code hoặc terminal của bạn.
Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.
Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.