Pesan Validasi yang Tertelan Error Generik
Service merangkai pesan validasi presisi, handler membalas pesan generik. Pola jembatan meneruskan detail statis hasil audit dengan regresi test.
Ringkasan
Service sudah kasih pesan error yang jelas soal batas 100 karakter, tapi helper respondErr menelannya jadi pesan generik. Jawaban generik memang disengaja demi keamanan, tapi aturan bisnis kayak gini justru wajib sampai ke pengguna biar nggak nebak-nebak. Solusinya: teruskan cuma detail yang sudah diaudit sebagai konstanta, lalu kunci pakai regresi test biar nggak balik generik lagi.
Dua Pesan untuk Satu Kegagalan
Wizard admin menyimpan satu field. Layar menjawab satu baris: "jenis atau status tidak valid". Field itu bukan jenis dan bukan status. Isinya nomor kwitansi, panjangnya 140 karakter, padahal batas service hanya 100.
Dugaan pertama: validasi panjang belum ada di service. Pemeriksaan bilang sebaliknya. Service mendeteksi pelanggaran itu dan menyusun pesan presisi, "receipt_number maksimal 100 karakter". Pesan itu sampai di handler, lalu hilang. Helper respondErr memetakan ErrInvalidInput ke satu string generik untuk semua kasus, dan detail presisi ikut tertelan.
Kenapa Pesan Generik Justru Disengaja
Menelan detail bukan kecelakaan, melainkan kebijakan keamanan yang lazim. Panduan OWASP menempatkan error handling sebagai bagian keamanan aplikasi: error yang bocor membantu rekonaisans penyerang[2] dan dapat mengungkap titik injeksi[2]. Respons ke klien sebaiknya generik; detail lengkap tinggal di log server.
Prinsip itu benar untuk detail yang berasal dari dunia luar: isi input pengguna, stack trace, nama tabel. Tapi pesan validasi "maksimal 100 karakter" bukan rahasia. Ia aturan bisnis yang justru wajib diketahui pengguna agar bisa memperbaiki input. Menerapkan kebijakan generik secara membuta membuat administrator menebak-nebak di wizard berisi banyak field.
Di sini letak sisi Go-nya. Pola membungkus satu error di dalam error lain sudah menjadi bagian resmi bahasa sejak Go 1.13[1]. Masalahnya, membungkus lewat fmt.Errorf dengan verb biasa membuang semua isi error asli kecuali teksnya[1]. Sentinel ErrInvalidInput tetap terdeteksi, tetapi detail presisi hanya hidup sebagai string di depannya. Konvensi repo memilih jalur itu, maka handler perlu langkah ekstra untuk meneruskan teksnya.
Pola Jembatan: Teruskan Hanya yang Sudah Diaudit
if errors.Is(err, ErrInvalidInput) {
// detail statis hasil audit semua wrap site: hanya konstanta, tanpa echo input
detail := strings.TrimSuffix(err.Error(), ": "+ErrInvalidInput.Error())
if detail != ErrInvalidInput.Error() {
response.Error(w, http.StatusBadRequest, "INVALID_INPUT", detail)
return
}
}
respondErr(w, err)
Pola ini aman dengan satu syarat yang mahal: audit. Setiap titik pembungkusan harus diperiksa bahwa detailnya konstanta, bukan hasil potongan input pengguna. Audit sekali saat menulis handler, lalu kunci dengan regresi test. Nama test di commit ini panjang dan spesifik dengan sengaja: TestPutOfficerFieldsOverCapShowsPreciseMessage. Jika seseorang kelak mengubah respondErr atau pola wrapping dan pesan presisi hilang lagi, test itu gagal sebelum pengguna sempat mengeluh.
Yang Diambil dari Kasus Ini
Keputusan antara aman dan jelas sering dipaksakan sebagai salah satu dari dua hal. Kasus ini menunjukkan pilihan ketiga: klasifikasikan pesan. Detail dinamis dari input dibuang ke log. Detail statis yang menjelaskan aturan bisnis diteruskan setelah diaudit. Keduanya dilindungi oleh mekanisme yang berbeda, log untuk yang pertama, regresi test untuk yang kedua.
Satu baris "receipt_number maksimal 100 karakter" menghabiskan lebih sedikit byte daripada pesan generik yang digantikannya. Kejelasan ternyata tidak pernah jadi mahal, yang mahal adalah kebingungan.
Sumber
- Working with Errors in Go 1.13 - The Go Programming Language (diakses 2026-10-12).
- Error Handling - OWASP Cheat Sheet Series (diakses 2026-10-12).