Batas Kolom Harus Dijaga di Validator, Bukan di MySQL
Validasi panjang harus meniru lebar kolom, bukan diserahkan ke database. Ketika 31 karakter menyentuh query, MySQL strict mode menjawab 500.
Ringkasan
API mengalami error 500 Internal Server Error. Error ini disebabkan oleh panjang karakter yang melebihi batas. Validator sudah diperbarui untuk menangani masalah ini.
Ada satu momen yang selalu sama di API dengan masalah ini: formulir admin KotaPortal menerima 31 karakter, tombol simpan jalan, lalu layar menjawab 500 Internal Server Error. Kolom tipe_cabor di api/migrations/000082_create_organizations.up.sql:14 memang dideklarasikan VARCHAR(30). Dugaan pertama tentu saja: validasi panjangnya belum ditulis.
Dugaan itu sebagian benar dan sebagian salah, dan bagian yang salah lebih menarik.
Yang benar, memang ada field yang belum dibatasi, dan validateUpsert di api/internal/organisasi/validator.go akhirnya menambahkan satu blok untuk itu:
if len(s) > 30 {
return fmt.Errorf("%w: tipe_cabor maksimal 30 karakter", ErrValidation)
}
Urutannya penting: pemotongan spasi dijalankan dulu, baru perpanjangan dibandingkan dengan 30. Cap yang sama sudah hidup untuk kontak di 100 karakter dan masa_bakti di 20 karakter, jadi daftar cap di file itu kini berbaris satu-satu dengan lebar kolomnya.
Yang meleset, validasi bukan satu-satunya penjaga yang berdiri di depan kolom. MySQL 8.0 mengaktifkan STRICT_TRANS_TABLES sebagai bagian dari mode SQL bawaannya, dan dokumentasinya, diarsipkan pada 2026-10-11 karena halaman manualnya sedang tidak tersedia, menegaskan posisi mode itu: "Strict SQL mode applies to the following errors", dengan penjelasan "A value is invalid if it has the wrong data type for the column or might be out of range" [1]. Nilai yang kepanjangan bukan data yang dipotong diam-diam; ia adalah kondisi yang ditolak. Nilai yang lolos dari validator tetap menabrak batas itu di ujung query.
Kenapa statusnya yang menentukan siapa yang disalahkan
Di sinilah letak kesalahannya. Handler sudah punya dua pintu keluar:
if errors.Is(err, ErrValidation) {
response.Error(w, http.StatusBadRequest, "VALIDATION", err.Error())
}
Nilai sentinel ErrValidation memutuskan pintu mana yang dipakai. Error yang melewati validator berakhir di pintu kedua, dan di sana tidak ada lagi yang menahan: pesan query keluar apa adanya, berstatus INTERNAL_ERROR, lengkap dengan detail yang tidak seharusnya dilihat pengguna formulir. Sebuah panjang 31 karakter hanya menyisakan dua pilihan di file itu, dan pilihan yang salah tidak punya tanda peringatan apa pun.
Perbedaan itu bukan kosmetik. 400 berarti permintaan klien memang salah, dan MDN menegaskan konsekuensinya: "Clients that receive a 400 response should expect that repeating the request without modification will fail with the same error" [3], sehingga klien berhenti mengulang dan memperbaiki inputnya. 500 berarti "the server encountered an unexpected condition that prevented it from fulfilling the request" [4], dan klien yang menerima itu tidak punya alasan untuk berhenti mencoba hal yang sama. Panjang 31 karakter yang menyentuh query langsung berpindah dari kelompok yang pertama ke kelompok yang kedua, tanpa ada yang memberi tahu siapa pun mengapa.
Uji yang dijalankan berpasangan
Perbaikannya tidak berhenti di validator. api/tests/unit/organisasi/service_test.go menambahkan kasus {"tipe_cabor 31", ...} yang wajib ditolak, dan pada saat yang sama memperpanjang permintaan batas memakai TipeCabor: strPtr(strings.Repeat("t", 30)) dengan catatan "boundary tepat batas harus lolos". Satu karakter dari keduanya adalah seluruh isi ujinya: sebuah cap yang menolak 30 karakter sekaligus sama bermasalahnya dengan cap yang tidak ada.
Daftar cap seperti ini memang terlihat seperti pekerjaan administratif: 30 di sini, 100 di sana, 20 di kolom lain. Namanya juga memang begitu. Setiap kali migrasi menambah kolom teks baru, baris capnya ikut bertambah, dan kasus uji batasnya ikut bertambah bersamanya. Satu baris di validator jauh lebih murah daripada satu laporan insiden yang menyebut pesan 500 di depan pengguna.
OWASP menaruh aturan yang sama dalam bentuk umum: untuk string, "Length limits and the characters or structure required by the field" [2], dengan prinsip "Define what the application accepts and reject values outside those rules" [2]. Lebar kolom adalah kontrak yang harus dibawa validator sampai ke batasnya, bukan sesuatu yang diserahkan ke pesan error database. Soal bentuk pesan yang keluar saat validasi ditolak, itu urusan terpisah yang sudah dibahas di pesan validasi yang tertelan error generik.
Catatan teknis: batas len(s) pada teks Go menghitung byte, sementara VARCHAR(30) menghitung karakter. Untuk nilai enum beraksen Latin yang dipakai di sini keduanya berimpit, dan perbedaan itu tidak diuji pada siklus ini.