Penanganan Error untuk Web

Menampilkan versus mencatat error serta halaman error kustom.

Daftar isi

Pesan error pada aplikasi web sampai ke dua pihak dengan kebutuhan yang berbeda, yaitu pengunjung yang membutuhkan halaman yang layak dan pemilik aplikasi yang membutuhkan catatan lengkap untuk mencari penyebab. Topik ini membahas pembagian tugas display_errors dan log_errors, penangan aktif untuk peringatan dan pengecualian, serta halaman error kustom yang mencatat detail ke log sambil menampilkan pesan generik. Setelah membacanya, kegagalan aplikasi web dapat diarahkan ke log dan ke halaman yang tepat tanpa membocorkan detail internal.

Menampilkan dan mencatat error#

Dua direktif konfigurasi menentukan nasib pesan error, yaitu display_errors dan log_errors. Direktif display_errors mengirim pesan error langsung ke keluaran yang sampai ke browser, sedangkan log_errors menyalin pesan yang sama ke tujuan yang ditunjuk direktif error_log. Keduanya dapat diatur melalui konfigurasi server maupun ini_set pada awal skrip.

DirektifPerilaku
display_errorsMenampilkan pesan error pada keluaran yang diterima browser.
log_errorsMencatat pesan error ke tujuan yang ditunjuk direktif error_log.
error_logJalur berkas atau tujuan pencatatan ketika log_errors aktif.

Lingkungan produksi menggunakan susunan yang berlawanan dengan lingkungan pengembangan, yaitu log_errors aktif dan display_errors mati. Pesan error yang tampil di halaman memberikan petunjuk berharga bagi orang yang mencari penyebab kegagalan, tetapi petunjuk yang sama berguna bagi orang yang mencari celah. Pencatatan menyimpan pesan untuk diperiksa kemudian, sedangkan halaman yang dilihat pengunjung tetap bersih dari detail internal.

Fungsi error_log mengirim satu pesan ke tujuan tertentu tanpa menunggu PHP mencatat error secara otomatis. Parameter message_type menentukan tujuannya, yaitu nilai 0 menyerahkan pesan ke mekanisme log PHP yang mengikuti direktif error_log, sedangkan nilai 3 menambahkan pesan apa adanya ke berkas yang disebut pada parameter destinasi. Percobaan berikut mencatat satu pesan ke /tmp/log-error-web.log, lalu menampilkan isi log itu.

php Mencatat pesan ke berkas log dengan error_log
<?php
declare(strict_types=1);

$berkasLog = '/tmp/log-error-web.log';

// mulai dari log yang kosong agar percobaan dapat diulang dengan hasil sama
if (is_file($berkasLog)) {
    unlink($berkasLog);
}

// message_type 3 menambahkan pesan apa adanya ke berkas yang ditunjuk parameter destinasi
error_log('Pencatatan dimulai dari skrip CLI.' . PHP_EOL, 3, $berkasLog);

echo 'Log ' . $berkasLog . ' berisi:' . PHP_EOL;
echo file_get_contents($berkasLog);
text Keluaran
Log /tmp/log-error-web.log berisi:
Pencatatan dimulai dari skrip CLI.

Arah log yang sama ditetapkan sekali untuk seluruh aplikasi melalui ini_set('error_log', '/tmp/log-error-web.log') di awal skrip atau melalui konfigurasi server. Selama log_errors aktif, setiap error otomatis berakhir di berkas itu dengan waktu dan label tingkat error pada setiap barisnya, sedangkan pemanggilan error_log dengan message_type 3 menulis pesan persis seperti yang diberikan.

Penangan aktif untuk peringatan dan pengecualian#

Fungsi set_error_handler memasang fungsi yang dipanggil PHP setiap kali muncul peringatan atau notice, sehingga tampilan bawaan digantikan oleh perilaku yang ditentukan sendiri. Pola yang lazim adalah melempar ErrorException dari dalam penangan, karena peringatan kemudian mengikuti jalur try/catch yang sama dengan pengecualian biasa dan tidak pernah muncul di tengah halaman.

php Mengubah peringatan menjadi pengecualian
<?php
declare(strict_types=1);

// peringatan dilempar sebagai ErrorException agar dapat ditangani try/catch
set_error_handler(static function (int $nomor, string $pesan, string $berkas, int $baris): bool {
    throw new ErrorException($pesan, 0, $nomor, $berkas, $baris);
});

try {
    $nilai = ['a' => 1];
    echo $nilai['kunci_lama'];
} catch (ErrorException $e) {
    echo 'Tertangkap: ' . $e->getMessage() . ' pada baris ' . $e->getLine() . PHP_EOL;
}

echo 'Skrip melanjutkan eksekusi setelah peringatan ditangani.' . PHP_EOL;
text Keluaran
Tertangkap: Undefined array key "kunci_lama" pada baris 11
Skrip melanjutkan eksekusi setelah peringatan ditangani.

Penangan menerima nomor error, pesan, berkas, dan baris pemicunya, lalu ErrorException membawa semuanya ke blok catch. Skrip melanjutkan eksekusi setelah peringatan tertangkap, dan baris penutup pada keluaran membuktikannya. Jangkauan penangan ini terbatas, karena error fatal seperti E_ERROR menghentikan skrip sebelum penangan sempat berjalan. Untuk keadaan itulah display_errors dan log_errors tetap menentukan apa yang ditampilkan dan dicatat.

Pengecualian yang lolos dari seluruh blok catch diteruskan ke PHP dan memicu halaman fatal bawaan. Fungsi set_exception_handler memasang penangan untuk keadaan itu, yaitu fungsi yang menerima satu Throwable dan menentukan sendiri apa yang dilaporkan.

php Menangani pengecualian tak tertangani
<?php
declare(strict_types=1);

// pengecualian yang tidak tertangani dialihkan ke fungsi ini
set_exception_handler(static function (Throwable $e): void {
    echo 'Pengecualian tak tertangani: ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
});

throw new RuntimeException('Pengaturan batas_unggah tidak ditemukan.');
text Keluaran
Pengecualian tak tertangani: RuntimeException: Pengaturan batas_unggah tidak ditemukan.

Keluaran berasal sepenuhnya dari penangan yang terpasang, dan eksekusi berhenti setelahnya karena pengecualian itu memang tidak tertangani. Halaman fatal bawaan tidak tampil, tetapi pengunjung juga belum mendapatkan respons yang layak, karena halaman seperti itu harus disusun oleh penangan yang terpasang. Cara menyusunnya menjadi halaman error kustom dibahas pada bagian berikutnya.

Halaman error kustom untuk web#

Halaman error kustom menyatukan dua tugas di satu tempat, yaitu mencatat detail untuk pemilik aplikasi dan menampilkan halaman generik untuk pengunjung. Logika halaman dibungkus try/catch(Throwable). Blok catch mencatat pesan beserta jejak (stack trace) ke log, lalu mengirim http_response_code(500) beserta HTML generik yang tidak menyebut nama berkas, baris, maupun isi pengecualian.

php proses-error.php
<?php
declare(strict_types=1);

if (PHP_SAPI === 'cli') {
    echo 'Berkas ini dirancang untuk berjalan melalui server web.' . PHP_EOL;
    exit;
}

const BERKAS_LOG = '/tmp/log-error-web.log';

// membaca satu pengaturan, kunci yang belum ada memicu pengecualian
function ambilPengaturan(string $kunci): string
{
    $pengaturan = ['nama_situs' => 'Aplikasi Catatan'];
    if (!array_key_exists($kunci, $pengaturan)) {
        throw new RuntimeException('Pengaturan ' . $kunci . ' tidak ditemukan.');
    }

    return $pengaturan[$kunci];
}

try {
    // halaman disusun di dalam blok try agar setiap kegagalan masuk ke catch
    $judul = ambilPengaturan('judul_halaman');
    $judulAman = htmlspecialchars($judul, ENT_QUOTES, 'UTF-8');
    http_response_code(200);
    echo '<!DOCTYPE html>' . PHP_EOL;
    echo '<html lang="id">' . PHP_EOL;
    echo '<head><meta charset="utf-8"><title>' . $judulAman . '</title></head>' . PHP_EOL;
    echo '<body><h1>' . $judulAman . '</h1></body>' . PHP_EOL;
    echo '</html>' . PHP_EOL;
} catch (Throwable $e) {
    // catat pesan dan jejak untuk pemilik aplikasi, bukan untuk pengunjung
    error_log($e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL, 3, BERKAS_LOG);

    // status 500 beserta halaman generik tanpa detail internal
    http_response_code(500);
    echo '<!DOCTYPE html>' . PHP_EOL;
    echo '<html lang="id">' . PHP_EOL;
    echo '<head><meta charset="utf-8"><title>Terjadi Kesalahan</title></head>' . PHP_EOL;
    echo '<body>' . PHP_EOL;
    echo '<h1>Terjadi kesalahan di server</h1>' . PHP_EOL;
    echo '<p>Permintaan tidak dapat diselesaikan. Silakan coba lagi nanti.</p>' . PHP_EOL;
    echo '</body>' . PHP_EOL;
    echo '</html>' . PHP_EOL;
}
text Keluaran
Berkas ini dirancang untuk berjalan melalui server web.

Pada percobaan, kunci judul_halaman sengaja belum ada di daftar pengaturan sehingga ambilPengaturan melempar RuntimeException. Server bawaan dijalankan pada port 8130 dari folder topik, lalu halaman yang memicu pengecualian diminta dengan curl untuk membaca kode statusnya.

bash
# mulai dari log yang kosong
rm -f /tmp/log-error-web.log
# jalankan server bawaan pada port 8130 dari folder topik
php -S localhost:8130 >/dev/null 2>&1 &
sleep 1
# periksa kode status HTTP halaman yang memicu pengecualian
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8130/proses-error.php
text Keluaran
500

Kode 500 dengan halaman generik adalah seluruh yang diterima pengunjung. Tidak ada nama kelas, jalur berkas, atau pesan pengecualian yang ikut terkirim, sehingga halaman tidak memberi petunjuk apa pun tentang bagian dalam aplikasi. Isi log yang dicatat oleh permintaan tadi dan halaman yang diterima pengunjung ditampilkan bersama pada blok berikut.

bash
# isi log yang dicatat saat pengecualian terjadi
cat /tmp/log-error-web.log
# halaman yang diterima pengunjung
curl -s http://localhost:8130/proses-error.php
# hentikan server bawaan
kill %1
text Keluaran
Pengaturan judul_halaman tidak ditemukan.
#0 /home/kal/my-project/site/content/php/pola-web/error-web/proses-error.php(24): ambilPengaturan('judul_halaman')
#1 {main}
<!DOCTYPE html>
<html lang="id">
<head><meta charset="utf-8"><title>Terjadi Kesalahan</title></head>
<body>
<h1>Terjadi kesalahan di server</h1>
<p>Permintaan tidak dapat diselesaikan. Silakan coba lagi nanti.</p>
</body>
</html>

Log memuat pesan beserta jejak yang menunjuk berkas dan baris pemicunya, sehingga penyebab kegagalan dapat dibuka tanpa mereproduksi permintaan dari nol. Halaman dan catatan log berasal dari satu blok catch yang sama, sehingga keduanya tidak mungkin berbeda isi untuk kegagalan yang sama.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat menata penanganan error untuk web.

  • display_errors dinyalakan untuk mencari penyebab kegagalan, lalu lupa dimatikan setelahnya. Pesan yang membocorkan jalur berkas dan potongan kode tetap tampil kepada pengunjung. Perbaikannya adalah memisahkan konfigurasi pengembangan dan produksi sejak awal, atau memeriksa log sebagai gantinya.
  • Isi variabel yang bersifat rahasia ikut dicatat ke log. Kata sandi, token, dan nomor kartu yang masuk log berumur panjang dan dibaca lebih banyak orang daripada yang disangka. Perbaikannya adalah mencatat pesan dan jejak saja, tanpa isi permintaan yang sensitif.
  • Jejak eksekusi dikirim ke halaman pengunjung karena blok catch menyertakan detail pengecualian pada respons. Detail yang sama memang berguna, tetapi hanya untuk pemilik aplikasi. Perbaikannya adalah mencatat jejak ke log dan menampilkan halaman generik pada respons.

Ringkasan#

  • display_errors menentukan apakah pesan error sampai ke browser, sedangkan log_errors beserta direktif error_log menentukan ke mana pesan itu dicatat, dan produksi menggunakan susunan log aktif dengan tampilan mati.
  • set_error_handler mengubah peringatan menjadi ErrorException yang mengikuti jalur try/catch, sedangkan set_exception_handler menangani Throwable yang lolos dari seluruh blok catch.
  • Halaman error kustom mencatat pesan beserta jejak ke log, mengirim status 500, dan menampilkan halaman generik tanpa detail internal.
  • Error fatal seperti E_ERROR tidak selalu terjangkau penangan kustom, sehingga konfigurasi display_errors dan log_errors tetap menjadi lapisan terakhir.

Referensi#