Beberapa minggu setelah situs membership klien live, saya iseng cek database. Tabel member: kosong. Bukan sepi, benar-benar nol. Tidak ada satu pun user baru yang pernah terdaftar sejak hari pertama. Padahal form register-nya ada, tampil normal, bisa disubmit, dan tidak pernah menampilkan error apa pun.
Dan bukan cuma register. Login member, lost password, sampai alur claim: semua form front-end yang dipakai user logged-out gagal dengan pola yang sama. Disubmit, halaman berpindah, lalu tidak terjadi apa-apa. Tidak ada error, tidak ada notice, tidak ada jejak di log. Gagal jenis paling menyebalkan: gagal diam-diam.
Jalan buntu: "di saya jalan"
Refleks pertama tentu tes sendiri. Saya buka situsnya, isi form register, submit. Jalan. Coba lost password: email reset terkirim. Coba alur claim: mulus. Semua hijau di tangan saya, padahal data bilang nol member pernah masuk.
Di titik ini gampang sekali menyimpulkan "user-nya yang salah pakai". Untungnya datanya terlalu ekstrem untuk kesimpulan itu. Satu dua user gagal itu wajar. Nol dari semua user selama berminggu-minggu itu bukan user error, itu sistem yang menolak semua orang kecuali saya.
Saya baca ulang handler form-nya baris per baris: valid. Markup form dan action URL-nya: benar, POST ke wp-admin/admin-post.php sesuai pakem WordPress. Hook admin_post_nopriv_*-nya terdaftar. Semuanya kelihatan benar, dan memang benar. Masalahnya bukan di situ.
Membedah request-nya, bukan kodenya
Karena kodenya kelihatan bersih, saya berhenti membaca kode dan mulai membedah request-nya. Saya kirim POST ke admin-post.php secara manual dengan redirect: 'manual' supaya redirect-nya tidak diikuti otomatis dan saya bisa membaca respons mentahnya:
const res = await fetch('https://situs-klien.test/wp-admin/admin-post.php', {
method: 'POST',
body: params,
redirect: 'manual',
});
console.log(res.status, res.headers.get('location'));Hasilnya: 302, dengan header Location mengarah ke halaman /account/. Bukan halaman sukses, bukan halaman error. Request-nya dibelokkan ke halaman login SEBELUM handler form sempat jalan sama sekali. Access log mengonfirmasi pola yang sama: POST masuk, langsung 302, handler tidak pernah tersentuh.
Redirect ke /account/ itu saya kenal betul. Itu buatan saya sendiri.
Root cause: admin-post.php ikut menjalankan admin_init
Beberapa waktu sebelumnya saya memasang hardening untuk wp-admin: semua user non-staff yang menyentuh area admin di-redirect keluar ke /account/. Implementasinya lewat hook admin_init:
add_action('admin_init', function () {
if (is_staff()) {
return;
}
wp_safe_redirect(home_url('/account/'));
exit;
});Niatnya sederhana: user biasa tidak punya urusan di dashboard wp-admin. Yang saya lewatkan: admin_init bukan cuma milik halaman dashboard. File wp-admin/admin-post.php, entry point resmi WordPress untuk memproses submit form publik, JUGA memanggil do_action('admin_init') di awal eksekusinya. Begitu juga admin-ajax.php.
Artinya: setiap kali user logged-out submit form register, login, lost password, atau claim, request-nya lewat admin-post.php, admin_init tereksekusi, is_staff() menjawab false, lalu wp_safe_redirect plus exit membunuh request sebelum satu baris handler pun berjalan.
Dan kenapa "di saya jalan"? Karena saya selalu login sebagai admin saat tes. is_staff() true, lockdown minggir, handler jalan normal. Bypass staff itulah yang membuat bug-nya invisible untuk developer dan fatal untuk semua orang lain.
Fix: exclude dua entry script publik
Perbaikannya kecil: lockdown-nya harus tahu bahwa admin-post.php dan admin-ajax.php bukan "halaman admin", melainkan pintu masuk publik yang kebetulan tinggal di folder wp-admin.
add_action('admin_init', function () {
if (in_array($GLOBALS['pagenow'] ?? '', ['admin-post.php', 'admin-ajax.php'], true)) {
return;
}
if (is_staff()) {
return;
}
wp_safe_redirect(home_url('/account/'));
exit;
});$GLOBALS['pagenow'] berisi nama file entry yang sedang dieksekusi. Dua file itu di-exclude, sisanya tetap terkunci rapat. Setelah deploy, saya ulangi tes POST manual: bukan 302 ke /account/ lagi, handler jalan, dan member pertama akhirnya benar-benar terdaftar.
Pelajaran
admin_inittidak berarti "hanya dashboard admin".admin-post.phpdanadmin-ajax.phpikut memanggilnya, dan keduanya adalah jalur form publik.- Kalau kamu tes fitur publik sambil login sebagai admin, kamu tidak sedang menguji apa yang dialami user. Selalu tes alur publik dari sesi logged-out atau incognito.
- Form yang gagal diam-diam paling cepat didiagnosa dari sisi HTTP, bukan sisi kode:
redirect: 'manual', baca status dan headerLocation, cocokkan dengan access log. - Angka ekstrem itu petunjuk. Satu dua kegagalan bisa jadi user error; nol keberhasilan total berarti sistemnya sendiri yang memblokir.
