Handler Menerima %2C: chi Tidak Mendekode Path Param
Slug dengan koma pulang 404 padahal datanya ada: chi menyimpan path param terenkode dan dekoding jadi kontrak pemilik API.
Ringkasan
Slug yang ada koma atau apostrof miring ternyata gagal 404 karena chi nyimpen path param masih terenkode, jadi lookup ke database nyari %2C padahal datanya ada. Solusinya simpel: bikin middleware DecodePathParams buat unescape semua path param sekali jalan. Dipasangnya lewat r.With biar jalan setelah routing tapi sebelum handler.
curl menuju endpoint detail sarana dengan slug panjang di URL itu pulang dengan 404. Yang membuatnya janggal: slug itu disalin persis dari baris tabel di database proyek KotaPortal. Tidak ada typo, tidak ada selisih karakter. Polanya pun tidak sendirian. Setiap slug yang memuat koma atau apostrof miring gagal dengan cara yang sama, sementara slug polos tanpa karakter khusus semua hidup.
Dugaan pertama mengarah ke dua tempat: proses impor data lama yang diduga meninggalkan slug rusak, atau double-encoding di sisi klien. Keduanya meleset. Yang paling membuka mata justru sifat 404-nya: router menemukan rutenya, handler sempat berjalan, baru lookup ke database yang kosong. Kalau rutenya memang tidak ada, yang muncul 404 dari router, bukan dari handler. Setelah menelusuri source code chi v5.1.0, router Go yang dipakai API ini, polanya kelihatan jelas: chi mencocokkan rute pada path dalam bentuk terenkode, yaitu r.URL.RawPath saat field tersebut terisi. Parameter rute sendiri baru dituangkan ke konteks permintaan setelah fungsi pencari rute selesai [2]. Akibatnya handler menerima koma sebagai %2C dan apostrof miring sebagai %E2%80%99, apa adanya. Pencarian ke database memakai string terenkode itu, jadi jelas tidak ketemu.
Dekode di Batas API, Bukan di Handler
Router tidak akan berubah, dan menyalin logika dekoding ke setiap handler berarti merawat kontrak yang sama di belasan tempat. Dekoding ditaruh di satu titik: middleware DecodePathParams pada subrouter utama API, dipasang lewat r.With. Pilihan ini bukan selera. Di source chi, middleware yang didaftarkan lewat Use dieksekusi sebelum router mencari rute, saat parameter rute masih kosong. Sementara pendaftaran lewat r.With mem-bake middleware ke endpoint yang sudah cocok, sehingga middleware berjalan setelah pencocokan dan sebelum handler [2]. Membaca dua fungsi di mux.go dan tree.go versi 5.1.0 menutup debatnya.
Setelah middleware ini jalan, chi.URLParam membaca nilai yang sudah didekode. Handler tidak menyentuh logika dekoding sama sekali, dan satu kontrak berlaku untuk seluruh path parameter di API.
Kontrak Middleware yang Ketat
Dekoding sembarangan justru membuka lubang. Maka kontraknya diikat empat aturan:
url.PathUnescape, bukanQueryUnescape: tanda plus di path harus tetap plus, bukan berubah jadi spasi [1].- Wildcard
*dilewati. Path unggahan berkas harus tetap mentah; hasil dekoding bisa merusak nama berkas atau menyintesis karakter traversal. - Dekode satu kali, sesuai RFC 3986 bagian 2.4: string yang sama tidak boleh didekode dua kali [3], jadi koma yang terenkode ganda tetap terlihat sebagai satu lapis escape.
- Escape tidak valid dan hasil bukan UTF-8 dibiarkan mentah. Lookup pun meleset ke 404 yang bersih, bukan 500 dari mode ketat MySQL.
Aturan ini menutup sisi klien sekaligus. Fungsi pengenkode URI standar di JavaScript sengaja meng-escape karakter sintaks URI dan direkomendasikan untuk field masukan pengguna [4]. Klien yang mengikuti anjuran dokumentasi justru mengirim path terenkode. Membuang dekoding dari kontrak API berarti menghukum klien yang berbuat benar.
curl -i "https://api-kotaportal.example.com/api/v1/entities/sarana-olahraga/gor-bulutangkis%2C-tenis-meja-dan-futsal"
func DecodePathParams(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if rctx := chi.RouteContext(r.Context()); rctx != nil {
keys, values := rctx.URLParams.Keys, rctx.URLParams.Values
for i, v := range values {
if keys[i] == "*" || !strings.Contains(v, "%") {
continue
}
d, err := url.PathUnescape(v)
if err != nil || !utf8.ValidString(d) {
continue
}
values[i] = d
}
}
next.ServeHTTP(w, r)
})
}
Bukti dari Router Nyata
Dua lapis pengujian menutup perbaikan ini. Unit test tabel berjalan di router chi sungguhan: koma terescape, apostrof Unicode, path tanpa persen yang harus identik, dan wildcard yang harus tetap mentah. Probe integrasi mereproduksi 404 dari handler saat middleware dilewati, lalu hijau saat dipasang kembali. Terakhir, permintaan langsung ke stack dev berubah dari 404 menjadi 200 pada slug yang sama yang tadi gagal.
Polanya mudah dikenali di API lain: lookup yang gagal padahal datanya ada, dan URL di log masih memuat %2C. Kalau router menyimpan parameter rute dalam bentuk terenkode, dekoding sebelum lookup adalah pekerjaan pemilik API. Menunggu router melakukannya berarti menunggu fitur yang tidak pernah dijanjikan.
Sources
[1] Go net/url package documentation
[2] chi v5.1.0 source code
[3] RFC 3986 section 2.4
[4] MDN encodeURIComponent documentation