URL Encoding & Decoding: Panduan Percent Encoding yang Developer Butuhkan
Anda buka log server, lalu menemukan ini di query string: %E4%BD%A0%E5%A5%BD. Data rusak? Bug? Bukan — itu karakter Cina 你好. Tiap karakter jadi tiga byte UTF-8, lalu percent-encoded supaya aman masuk URL. Kalau Anda web developer, pasti pernah kena: tampilan URL terlihat rusak, padahal justru berfungsi sesuai desain.
URL encoding — nama formalnya percent encoding — membuat karakter spesial aman untuk URL. Artikel ini membedah cara kerjanya sampai ke level byte, kapan pakai encodeURI versus encodeURIComponent, encoding yang benar di empat bahasa pemrograman, dan bug-bug klasik yang masih menjebak developer berpengalaman.
Tempel URL apa pun ke URL Decoder & Encoder kami untuk melihat encoding dan decoding secara real time sambil mengikuti panduan ini.
Apa Itu URL Encoding (Percent Encoding)?
URL cuma bisa memuat subset kecil dari ASCII. Huruf, angka, dan beberapa simbol lolos tanpa masalah. Sisanya — spasi, ampersand, teks Cina, emoji — harus dikonversi ke format yang URL bisa bawa.
Caranya: ganti tiap byte yang tidak aman dengan % plus dua digit hex. Spasi jadi %20. Ampersand jadi %26. Nama “percent encoding” datang dari awalan % ini.
Spesifikasinya ada di RFC 3986, terbit 2005 dan masih berlaku sampai sekarang. RFC ini menggantikan RFC 2396 — memperketat definisi karakter mana yang aman, mana yang reserved, dan bagaimana menangani teks non-ASCII.
Contoh singkat:
| Input | Encoded | Alasan |
|---|---|---|
hello world | hello%20world | Spasi tidak diizinkan dalam URL |
price=10&tax=2 | price%3D10%26tax%3D2 | = dan & punya makna struktural |
中 | %E4%B8%AD | Non-ASCII → byte UTF-8 → percent-encoded |
🚀 | %F0%9F%9A%80 | Emoji → 4 byte UTF-8 → percent-encoded |
Karakter Mana yang Perlu Di-encode?
RFC 3986 membagi karakter jadi tiga kelompok. Paham tiga kelompok ini bisa menghemat jam-jam debugging.
Karakter Unreserved (Tidak Pernah Di-encode)
66 karakter ini boleh langsung masuk di bagian mana pun dari URL:
A-Z a-z 0-9 - . _ ~
Itu saja. Huruf, angka, hyphen, titik, underscore, tilde. Tidak ada yang lain.
Karakter Reserved (Tergantung Konteks)
Karakter-karakter ini jadi delimiter struktural dalam URL:
| Karakter | Peran dalam struktur URL |
|---|---|
: | Memisahkan scheme dari authority (https:) |
/ | Memisahkan segmen path |
? | Memulai query string |
# | Memulai fragment |
& | Memisahkan parameter query |
= | Memisahkan key dari value parameter |
@ | Memisahkan userinfo dari host |
+ ! $ ' ( ) * , ; [ ] | Berbagai peran reserved lainnya |
Aturan simpelnya: kalau karakter reserved sedang menjalankan fungsi strukturalnya, biarkan. Kalau muncul sebagai data (misal di dalam value parameter), encode.
Sisanya (Selalu Di-encode)
Spasi, angle bracket, kurung kurawal, pipe, backslash, karakter non-ASCII (Cina, Arab, emoji) — semua wajib percent-encode sebelum masuk URL.
Spasi perlu perhatian ekstra: RFC 3986 encode jadi %20, tapi form HTML pakai +. Soal konflik ini dibahas di bagian bawah.
Cara Kerja URL Encoding: Pipeline UTF-8
Untuk ASCII, prosesnya langsung: cari nilai byte dalam hex, tempel % di depan. Spasi (byte 32, hex 20) jadi %20.
Teks non-ASCII butuh tiga langkah:
Langkah 1 — Karakter ke code point Unicode.
é punya code point U+00E9. Emoji 🚀 ada di U+1F680.
Langkah 2 — Code point ke byte UTF-8.
UTF-8 pakai 1 sampai 4 byte tergantung rentang code point. é (U+00E9) jadi dua byte: 0xC3 0xA9. Roket (U+1F680) jadi empat byte: 0xF0 0x9F 0x9A 0x80.
Langkah 3 — Tiap byte jadi %XX.
Masing-masing byte dari langkah 2 dapat triplet percent-encoded sendiri.
Berikut pipeline lengkap untuk beberapa tipe karakter:
| Karakter | Code Point | Byte UTF-8 | Encoded | Pengali ukuran |
|---|---|---|---|---|
A | U+0041 | 41 | A (tidak di-encode) | 1× |
| spasi | U+0020 | 20 | %20 | 3× |
é | U+00E9 | C3 A9 | %C3%A9 | 6× |
中 | U+4E2D | E4 B8 AD | %E4%B8%AD | 9× |
🚀 | U+1F680 | F0 9F 9A 80 | %F0%9F%9A%80 | 12× |
Verifikasi sendiri di JavaScript:
const char = '中';
const encoded = encodeURIComponent(char);
console.log(encoded); // '%E4%B8%AD'
// Trace the bytes
const bytes = new TextEncoder().encode(char);
console.log([...bytes].map(b => '%' + b.toString(16).toUpperCase()).join(''));
// '%E4%B8%AD' — matches
Ekspansi ini relevan saat bicara batas panjang URL. Cuma 20 karakter Cina sudah menambah 180 karakter percent-encoded.
encodeURI vs encodeURIComponent — Memilih Fungsi yang Tepat
Ini kesalahan URL encoding paling umum di JavaScript. Nama mirip, tapi set karakter yang mereka encode sangat berbeda.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| Tujuan | Meng-encode URL lengkap | Meng-encode satu komponen (key atau value parameter) |
| Mempertahankan | : / ? # & = @ + $ , | Tidak satu pun dari karakter di atas |
| Meng-encode | Spasi, non-ASCII, beberapa tanda baca | Semua kecuali A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| Gunakan ketika | Anda punya URL lengkap dengan spasi atau Unicode di path | Anda membangun query parameter dari input pengguna |
Bug ini masuk production lebih sering dari yang developer mau akui:
// ❌ BUG: encodeURI does NOT encode &
const search = 'Tom & Jerry';
const bad = `https://api.example.com/search?q=${encodeURI(search)}`;
// Result: https://api.example.com/search?q=Tom%20&%20Jerry
// The & splits the query string — server sees q=Tom%20 and a separate param %20Jerry
// ✅ FIX: encodeURIComponent encodes & as %26
const good = `https://api.example.com/search?q=${encodeURIComponent(search)}`;
// Result: https://api.example.com/search?q=Tom%20%26%20Jerry
Ragu pilih yang mana? Pakai encodeURIComponent(). Benar untuk 95% kasus pembuatan URL di dunia nyata.
Coba kedua mode secara berdampingan di tool URL Encoder kami →
URL Encoding di Setiap Bahasa
JavaScript (Browser & Node.js)
// Encode a parameter value
const value = encodeURIComponent('price >= 100 & currency = €');
// 'price%20%3E%3D%20100%20%26%20currency%20%3D%20%E2%82%AC'
// Decode
const original = decodeURIComponent(value);
// 'price >= 100 & currency = €'
// Modern approach: URLSearchParams handles encoding automatically
const params = new URLSearchParams({ q: 'hello world', lang: '中文' });
console.log(params.toString());
// 'q=hello+world&lang=%E4%B8%AD%E6%96%87'
// Note: URLSearchParams uses + for spaces (form encoding)
Python
from urllib.parse import quote, unquote, urlencode
# Encode a path segment
quote('hello world/file name.txt', safe='/')
# 'hello%20world/file%20name.txt'
# Encode query parameters
urlencode({'q': '你好', 'page': '1'})
# 'q=%E4%BD%A0%E5%A5%BD&page=1'
# quote_plus uses + for spaces (form encoding)
from urllib.parse import quote_plus
quote_plus('hello world') # 'hello+world'
quote('hello world') # 'hello%20world'
Go
import "net/url"
// Encode a query value (uses + for spaces)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"
// Encode a path segment (uses %20 for spaces)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"
// Build a URL safely with url.Values
params := url.Values{}
params.Set("q", "你好世界")
params.Set("page", "1")
fmt.Println(params.Encode())
// "page=1&q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"
Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Encode (uses + for spaces — Java follows form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"
// For RFC 3986 compliance, replace + with %20
String rfc3986 = encoded.replace("+", "%20");
// "hello%20world%20%26%20more"
// Decode
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"
Go dan Java default ke form encoding (spasi jadi +). Butuh output RFC 3986? Ganti + dengan %20 setelah encoding.
Lima Bug URL Encoding yang Merusak Production
1. Double Encoding (%2520 Alih-alih %20)
Anda encode string, lalu framework encode lagi. % dalam %20 ikut ter-encode jadi %25. Server sekarang lihat literal %20, bukan spasi.
Gejala: URL berisi %2520, %253D, atau pola %25xx lainnya.
Diagnosis: Ada %25 di URL? Hampir pasti double encoding — karakter % sendiri ikut ter-encode.
Solusi: Decode dulu, baru encode sekali. Jangan encode string tanpa cek apakah sudah ter-encode.
// Detect double encoding
function isDoubleEncoded(str) {
return /%25[0-9A-Fa-f]{2}/.test(str);
}
// Safe encode: decode first, then encode
function safeEncode(str) {
try { str = decodeURIComponent(str); } catch (e) { /* not encoded, that's fine */ }
return encodeURIComponent(str);
}
2. + di Segmen Path
Developer encode nama file pakai library yang menghasilkan + untuk spasi. my report.pdf jadi my+report.pdf. Server baca + sebagai plus literal — 404.
Intinya: + artinya spasi cuma di query string (setelah ?). Di path, + ya tetap +. Selalu pakai %20 untuk spasi di path.
3. Redirect URI OAuth yang Rusak
URL otorisasi kayak gini:
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
Server OAuth baca redirect_uri=https://myapp.com/callback?code=abc dan anggap state=xyz sebagai parameter top-level terpisah. Auth gagal.
Solusi: Encode seluruh value redirect URI:
const redirectUri = 'https://myapp.com/callback?code=abc&state=xyz';
const authUrl = `https://auth.provider.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;
// redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fcode%3Dabc%26state%3Dxyz
4. Teks Non-ASCII Rusak di Log
Log server menampilkan %E4%BD%A0%E5%A5%BD alih-alih karakter Cina yang bisa dibaca. Ini bukan bug — URL-nya memang ter-encode benar. Yang salah: log viewer Anda tidak men-decode sequence percent-encoded.
Solusi: Alirkan log melalui decoder, atau tempel URL ke URL Decoder untuk membaca teks aslinya.
5. Kegagalan Signing API
OAuth 1.0 dan AWS Signature V4 butuh encoding RFC 3986 yang ketat. Masalahnya, encodeURIComponent() tidak encode !, ', (, ), atau *. Kalau karakter-karakter ini ada di input signing, signature pasti tidak cocok.
Solusi: Tambah post-processing:
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 vs + — Dilema Encoding Spasi
Dua standar, satu karakter, kebingungan tanpa akhir.
| Standar | Spasi menjadi | Berlaku di mana |
|---|---|---|
| RFC 3986 (sintaks URI) | %20 | Di mana pun dalam URL |
application/x-www-form-urlencoded | + | Query string dari pengiriman form HTML |
Konvensi + warisan dari awal mula web. Saat browser kirim <form> HTML dengan method="GET", spasi di-encode jadi + di query string. Perilaku ini sudah baked in di spesifikasi HTML — tidak akan hilang.
Tapi hati-hati: + cuma berarti “spasi” di query string. Di path, + ya tanda plus literal. Makanya https://example.com/my+file.pdf menyajikan file bernama my+file.pdf, bukan my file.pdf.
Panduan praktis:
- Pakai
%20saat bangun URL manual atau encode path segment. Aman di mana saja. - Terima
+waktu parsing query string dari form — framework Anda kemungkinan sudah handle ini. - Jangan campur. Satu konvensi per komponen, konsisten.
URL Encoding dan Keamanan
URL Encoding BUKAN Enkripsi
Percent encoding itu transformasi reversible yang deterministik. Tidak ada key, tidak ada secret, nol keamanan. Siapa pun bisa decode %48%65%6C%6C%6F balik jadi Hello dalam milidetik.
Jangan pakai URL encoding untuk sembunyikan data sensitif. Pakai HTTPS untuk enkripsi seluruh request. Ingat juga: URL muncul di log server, riwayat browser, dan header Referer — data sensitif sebaiknya masuk body request, bukan URL.
Serangan Open Redirect
Penyerang craft URL ter-encode untuk lolos dari validasi sederhana. Parameter redirect berisi %2F%2Fevil.com akan ter-decode jadi //evil.com — browser menafsirkannya sebagai URL protocol-relative ke domain penyerang.
Pertahanan: Validasi URL yang sudah di-decode, bukan bentuk ter-encode. Pakai allowlist untuk domain redirect.
Eksploitasi Double Encoding
WAF cek URL masuk untuk tag <script>. Penyerang kirim %253Cscript%253E — WAF lihat teks percent-encoded, lolos. Aplikasi decode sekali jadi %3Cscript%3E, decode kedua menghasilkan <script>. Filter terlewat.
Pertahanan: Normalisasi semua input (decode sepenuhnya) sebelum jalankan security check. Jangan andalkan satu kali decode.
Mau pelajari lebih dalam soal keamanan web? Baca panduan Esensial Keamanan Web kami.
Batas Panjang URL dan Kapan Encoding Menjadi Mahal
Spesifikasi HTTP tidak definisikan panjang URL maksimum. Tapi tiap lapisan stack punya batas praktis masing-masing.
| Lapisan | Batas |
|---|---|
| Rekomendasi umum | 2.000 karakter |
| Chrome, Firefox | ~2 MB (tapi server menolak jauh sebelum ini) |
| Apache (default) | 8.190 byte |
| Nginx (default) | 8.192 byte |
| IIS | 16.384 byte (query string) |
| CDN, proxy | Bervariasi — sering 4.096-8.192 byte |
Percent encoding bikin URL makin panjang. Satu karakter Cina mengembang dari 1 jadi 9 karakter (%E4%B8%AD). Emoji jadi 12. URL dengan 200 karakter Cina di query string sudah 1.800 karakter percent-encoded — belum termasuk base URL.
Kalau mentok batas: Pindahkan data dari query parameter ke body request via POST. Untuk fitur pencarian, buat endpoint POST yang terima body JSON berisi kriteria pencarian.
FAQ
Apa itu URL encoding dan mengapa developer membutuhkannya?
URL encoding (percent encoding) mengonversi karakter yang tidak boleh ada di URL jadi sequence hex %XX. URL cuma support 66 karakter ASCII unreserved — spasi, ampersand, teks Unicode, dan kebanyakan tanda baca harus di-encode supaya tidak merusak struktur URL atau bikin server salah interpretasi.
Apa perbedaan antara encodeURI dan encodeURIComponent?
encodeURI() encode URL lengkap tapi pertahankan karakter struktural (://, /, ?, &). encodeURIComponent() encode semuanya kecuali A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Pakai encodeURIComponent() untuk value query parameter. encodeURI() cuma cocok kalau Anda punya URL lengkap dan mau perbaiki spasi atau karakter non-ASCII tanpa merusak struktur.
Mengapa %20 terkadang muncul sebagai + di URL?
Keduanya representasi spasi, tapi dari standar berbeda. %20 ikut RFC 3986, berfungsi di semua komponen URL. + ikut spesifikasi form encoding HTML, cuma valid di query string. Di path, + tetap tanda plus literal. %20 selalu aman; + konvensi lama yang bertahan gara-gara perilaku form HTML.
Bagaimana cara URL-encode teks di Python, JavaScript, Go, dan Java?
JavaScript: encodeURIComponent('hello world') menghasilkan hello%20world. Python: urllib.parse.quote('hello world') menghasilkan hello%20world. Go: url.QueryEscape("hello world") menghasilkan hello+world. Java: URLEncoder.encode("hello world", UTF_8) menghasilkan hello+world. Go dan Java default ke form encoding (spasi jadi +) — ganti + dengan %20 kalau butuh output RFC 3986.
Bisakah URL encoding digunakan untuk keamanan atau enkripsi?
Tidak. URL encoding sepenuhnya reversible tanpa key — siapa saja bisa decode seketika. Nol kerahasiaan. Lindungi data sensitif pakai HTTPS (enkripsi TLS), bukan percent encoding. URL muncul di log server, riwayat browser, dan header Referer — data sensitif masukkan ke body request, bukan URL.
Apa itu double encoding dan bagaimana cara memperbaikinya?
Double encoding terjadi saat string yang sudah ter-encode kena encode lagi. % dalam %20 jadi %25, hasilnya %2520. Server lihat literal %20, bukan spasi. Fix-nya: decode input dulu, baru encode sekali. Pola %25 diikuti dua digit hex — tanda pasti double encoding.
Berapa panjang URL maksimum?
Spesifikasi HTTP tidak tetapkan batas resmi, tapi 2.000 karakter jadi patokan aman untuk kompatibilitas luas. Apache default 8.190 byte, Nginx 8.192 byte. Karakter non-ASCII mengembang 3-12x saat percent-encode, jadi URL internasional lebih cepat mentok batas. Untuk payload besar, pakai POST dengan body request.