Sebuah platform booking properti yang saya pegang untuk klien punya satu form yang penting: halaman "edit listing" untuk pemilik. Owner membuka halaman itu, mengubah deskripsi, harga per malam, dan aturan rumah, lalu klik simpan. Tapi masalahnya bukan di tombol simpan. Masalahnya, halaman edit itu sendiri selalu balik HTTP 500 begitu dibuka. Bukan sesekali, tapi setiap kali, untuk setiap listing.
Yang bikin bingung: 500-nya datang telat. Sepersekian detik saya sempat melihat form-nya hampir utuh di layar, field nama, harga, dan deskripsi semua terisi, sebelum halaman berganti jadi layar "critical error". Jadi ini bukan crash di awal request yang bikin halaman kosong dari atas. Ini crash yang meledak justru setelah sebagian besar halaman selesai dirender.
Gejalanya
Reaksi pertama saya salah arah. Karena ini form edit, saya kira handler simpannya yang bermasalah, jadi saya utak-atik jalur POST-nya dulu. Ternyata 500-nya muncul di GET, saat halaman baru dibuka, sebelum ada satu pun data yang dikirim balik. Fungsi simpan bahkan tidak sempat dipanggil.
Dugaan kedua: satu listing yang datanya korup. Saya buka listing lain, listing yang baru dibuat, listing lama, semuanya sama. Semua halaman edit 500. Kalau semua listing kena dengan cara yang persis sama, ini bukan soal satu baris data busuk, ini soal kode yang jalan di setiap render.
Di titik ini saya butuh satu hal: pesan error aslinya. Dan di sinilah situs klien ini menyulitkan. Hostingnya terkunci: tidak ada SSH, tidak ada FTP, dan error_log produksinya tidak bisa saya jangkau. File debug default di wp-content diblok dari akses HTTP, jadi saya tidak bisa sekadar membuka log lewat browser. Saya praktis buta. WordPress cuma menampilkan halaman "critical error" generik tanpa detail apa pun.
Menangkap fatal-nya tanpa SSH
Karena tidak bisa membaca log dengan cara normal, saya bikin jalan sendiri. Saya unggah satu mu-plugin sementara yang tugasnya cuma satu: mengalihkan error log ke file yang bisa saya ambil lewat HTTP, dengan nama acak supaya tidak gampang ditebak orang lain.
// wp-content/mu-plugins/00-capture.php (sementara, hapus setelah selesai)
ini_set( 'log_errors', '1' );
ini_set( 'error_log', WP_CONTENT_DIR . '/uploads/dbg-8f3a91.log' );
error_reporting( E_ALL );mu-plugin dijalankan lebih awal dan tidak bisa dimatikan dari dashboard, jadi ini tempat aman untuk menangkap fatal yang terjadi sebelum plugin lain sempat ikut campur. Saya buka lagi halaman edit yang 500, lalu tarik file log-nya langsung dari browser:
https://situs-klien.example/wp-content/uploads/dbg-8f3a91.logDan akhirnya pesan aslinya keluar, jelas dan tidak ambigu:
PHP Fatal error: Uncaught TypeError: esc_textarea(): Argument #1 ($text)
must be of type string, array given, called in
.../template-owner-add-property.php on line ...Akar masalahnya: house_rules ternyata array
Petunjuknya lengkap. Template form memanggil esc_textarea( $property['house_rules'] ) untuk mengisi textarea aturan rumah. Asumsi saya, dan asumsi siapa pun yang menulis baris itu, house_rules adalah string biasa. Ternyata tidak.
Data listing ini bolak-balik lewat lapisan REST, dan di sana house_rules yang aslinya teks bebas di-cast menjadi array. Nilainya bukan "No smoking", tapi [''], sebuah array berisi baris-baris. Jadi saat template membacanya kembali, $property['house_rules'] adalah array, bukan string.
Di PHP 8, esc_textarea() punya type hint string di argumennya. Kasih array, dan dia bukan cuma memberi warning, dia melempar TypeError. TypeError yang tidak ditangkap adalah fatal error. Dan inilah yang menjelaskan gejala "500 yang datang telat": fatal itu tidak menghentikan request di tempat yang terlihat rapi. Handler fatal WordPress berjalan di fase shutdown lewat register_shutdown_function, setelah PHP selesai mengeksekusi dan sebagian output sudah terkirim. Field-field di atas textarea aturan rumah sudah sempat dirender dan ter-flush ke browser, lalu crash-nya baru mengubah akhir respons menjadi 500. Itu sebabnya halaman terlihat "hampir jadi" sepersekian detik sebelum berubah jadi error.
Perbaikannya
Perbaikannya kecil dan tepat di titik pakai: paksa house_rules menjadi string sebelum diserahkan ke esc_textarea().
// house_rules bisa balik sebagai [''] dari cast REST -> esc_textarea(array) = TypeError
$house_rules = is_array( $property['house_rules'] )
? implode( "\n", $property['house_rules'] )
: $property['house_rules'];
echo esc_textarea( $house_rules );implode( "\n", ... ) menggabungkan array jadi teks multi-baris yang justru cocok untuk textarea, dan cabang is_array() membuatnya aman untuk kedua bentuk data, string maupun array. 500-nya langsung hilang di semua halaman edit sekaligus.
Setelah itu, langkah terakhir yang tidak boleh lupa: hapus mu-plugin penangkap tadi beserta file log-nya. Membiarkan file log yang bisa di-fetch publik tergeletak di uploads itu bocor informasi. Nama acak cuma menunda, bukan mengamankan.
Pelajaran
TypeErroryang muncul lewat handler shutdown WordPress terasa intermiten dan misterius padahal 100 persen deterministik. Kalau 500 datang setelah halaman hampir jadi, curigai fatal di tengah render, bukan crash di awal request.- Jangan pernah berasumsi isi sebuah field selalu string. Data yang lewat REST atau lapisan serialisasi lain bisa berubah tipe diam-diam, dan
['']terlihat mirip string sampai fungsi bertipe ketat menolaknya. - Di PHP 8,
esc_textarea(),esc_html(), dan kawan-kawan punya type hintstring. Kasih array, dapat fatal, bukan sekadar warning seperti di PHP lama. - Coerce di titik pakai:
is_array( $v ) ? implode( "\n", $v ) : $vsebelumesc_textarea(). Murah, lokal, dan tidak mengganggu bagaimana data disimpan. - Kalau produksi tidak punya SSH atau FTP dan log-nya diblok HTTP, mu-plugin sementara yang
ini_seterror_logke file bernama acak diuploadsadalah cara cepat membaca fatal. Selalu hapus lagi setelah selesai.
