Pengembangan Web

Penanganan Error Production untuk Aplikasi PHP Kecil - Tampilkan Lebih Sedikit, Pahami Lebih Banyak

Penanganan Error Production untuk Aplikasi PHP Kecil - Tampilkan Lebih Sedikit, Pahami Lebih Banyak

Sebuah halaman PHP gagal di tengah pemrosesan request. Apa yang seharusnya diterima pengunjung, dan apa yang perlu disimpan operator? Kedua pertanyaan itu berkaitan, tetapi jawabannya tidak semestinya sama. Pengunjung membutuhkan respons yang stabil dan tidak membocorkan bagian dalam aplikasi. Orang yang mendiagnosis kegagalan memerlukan cukup bukti privat untuk menemukan jalur yang rusak.

Pemisahan ini mudah terlewat pada aplikasi kecil. Menampilkan semua error memang memudahkan development, tetapi membocorkan detail di production. Menyembunyikan semua error tanpa merekamnya menimbulkan masalah sebaliknya: dinding kosong yang sopan tanpa jejak berguna di belakangnya. Desain yang lebih dapat dipertanggungjawabkan menghasilkan dua keluaran dari satu kegagalan tak terduga: respons HTTP yang terbatas dan event diagnostik yang terlindungi.

Artikel ini mengembangkan pola tersebut untuk aplikasi PHP kecil. Ini sengaja dibuat sebagai baseline, bukan platform monitoring lengkap. Framework mungkin sudah menyediakan handler yang terintegrasi lebih baik, dan setiap deployment tetap memerlukan keputusan sendiri mengenai retention, access control, serta alerting.

Pisahkan dahulu kegagalan yang diperkirakan dan yang tak terduga

Tidak setiap request yang gagal berarti aplikasi mengalami crash. Artikel yang tidak ditemukan, field form yang tidak valid, login kedaluwarsa, dan pemeriksaan authorization yang gagal adalah hasil yang dapat diperkirakan aplikasi. Kondisi itu layak ditangani secara eksplisit di dekat code terkait, diberi status 4xx yang sesuai bila berlaku, dan disertai pesan yang membantu pengguna mengambil langkah masuk akal berikutnya.

Kegagalan tak terduga berbeda. Defect pemrograman, invariant yang rusak, atau dependency yang tidak tersedia dapat menghalangi server menyelesaikan request yang valid. RFC 9110 mendefinisikan respons 5xx sebagai server error dan menjelaskan 500 sebagai kondisi tak terduga yang menghalangi pemenuhan request. Karena itu, 500 cocok sebagai status jalan terakhir untuk kegagalan server-side yang tidak tertangani. Status ini tidak semestinya menjadi keranjang praktis untuk semua input yang ditolak.

Perbedaan ini penting secara operasional. Jika kesalahan validasi biasa menjadi respons 500, insiden nyata akan lebih sulit terlihat di tengah noise. Jika crash sungguhan mengembalikan 200, client, uptime check, dan access log dapat salah menganggap kegagalan sebagai keberhasilan. Status code adalah bagian dari diagnosis, bukan hiasan di sekitar halaman error.

Konfigurasikan PHP agar melaporkan tanpa membocorkan

Penanganan error production dimulai di bawah handler aplikasi. Dokumentasi runtime configuration PHP menyatakan bahwa display_errors adalah fitur development dan tidak boleh digunakan pada sistem production. Dokumentasi itu secara terpisah menganjurkan error logging alih-alih error display untuk website production.

Postur production yang konservatif tampak seperti ini:

; production php.ini or an appropriate PHP-FPM configuration
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On

error_reporting = E_ALL tidak berarti setiap diagnostik harus tampil di browser. Reporting, displaying, dan logging adalah kontrol yang terpisah. Reporting yang luas dengan display nonaktif memungkinkan diagnostik mencapai jalur logging yang dikonfigurasi tanpa mengubah respons menjadi layar debugging.

Tujuan persisnya bergantung pada deployment. PHP dapat menggunakan error_log yang dikonfigurasi secara eksplisit, logger SAPI, atau system logger, tergantung konfigurasi. Tujuan tersebut membutuhkan read access yang dibatasi, write permission yang berfungsi, rotation, dan ruang kosong yang memadai. Setting yang menyatakan logging aktif bukan bukti bahwa record benar-benar tersimpan dengan baik.

Setting ini sebaiknya berada pada konfigurasi server, bukan hanya di code aplikasi. PHP mencatat bahwa mengubah display_errors saat runtime tidak dapat memengaruhi fatal error yang terjadi sebelum pemanggilan terkait dijalankan. Syntax failure dalam file bootstrap tidak dapat diperbaiki oleh baris ini_set() yang tidak pernah dicapai PHP.

Tambahkan handler jalan terakhir yang kecil

set_exception_handler() milik PHP mendaftarkan callback untuk Throwable yang tidak tertangkap. Interface Throwable mencakup Error dan Exception. Setelah callback berjalan, eksekusi berhenti; ini adalah batas untuk terminasi yang rapi, bukan cara melanjutkan request yang gagal.

Contoh tanpa framework berikut membuat correlation ID sebelum mendaftarkan handler minimal. Contoh ini merekam field yang konservatif, menetapkan status secara eksplisit, dan mengembalikan respons HTML generik:

<?php
declare(strict_types=1);

$requestId = bin2hex(random_bytes(8));

set_exception_handler(static function (Throwable $error) use ($requestId): void {
    $event = [
        'time' => gmdate('c'),
        'level' => 'error',
        'event' => 'uncaught_throwable',
        'request_id' => $requestId,
        'type' => $error::class,
        'file' => $error->getFile(),
        'line' => $error->getLine(),
    ];

    error_log((string) json_encode(
        $event,
        JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
    ));

    if (!headers_sent()) {
        http_response_code(500);
        header('Content-Type: text/html; charset=UTF-8');
        header('Cache-Control: no-store');
    }

    $safeId = htmlspecialchars($requestId, ENT_QUOTES, 'UTF-8');
    echo '<h1>Something went wrong</h1>';
    echo '<p>Please try again later. Reference: ' . $safeId . '</p>';
});

Referensi publik dan request_id privat memungkinkan operator mencari event terkait ketika pengunjung melaporkan masalah. Seperti dijelaskan OWASP Logging Cheat Sheet, interaction identifier membantu menghubungkan event yang berasal dari satu interaksi. Nilai ini hanya untuk korelasi: bukan authentication, authorization, ataupun secret.

Pemanggilan eksplisit http_response_code(500) penting karena status default PHP dalam konteks web server adalah 200 hingga status lain ditetapkan. Pemeriksaan headers_sent() mengakui keterbatasan alih-alih menyembunyikannya. Jika output sudah dimulai, handler mungkin tidak dapat mengganti status dan header. Output buffering dan front controller dapat mengurangi risiko itu, tetapi pilihan yang benar bergantung pada aplikasinya.

Catat secukupnya, bukan semuanya

Mungkin terasa menggoda untuk menambahkan seluruh exception message, stack trace, request body, header, session, dan user record. Konteks yang lebih banyak dapat mempercepat debugging, tetapi konteks tanpa seleksi menciptakan database sensitif kedua yang menyamar sebagai file teks.

OWASP menyarankan agar password, access token, session identifier, database connection string, encryption key, dan data pribadi sensitif biasanya tidak dicatat secara langsung. Exception message juga dapat berisi teks SQL, filesystem path, respons remote, atau nilai yang dikendalikan pengguna. Karena itu, contoh tadi dimulai dengan event type, class, lokasi code, waktu, dan correlation ID. Aplikasi dapat menambahkan field yang dipilih dengan hati-hati setelah asal data dan aturan redaction-nya dipahami.

Teks tidak tepercaya juga memengaruhi integritas log. Carriage return, line feed, dan delimiter dapat memalsukan entry atau merusak downstream parser. Encoding JSON terstruktur lebih mudah diprediksi daripada string delimiter buatan tangan, tetapi encoding saja tidak menentukan apakah suatu field aman atau diperlukan. Validasi panjang field, buang control character terlarang bila sesuai, dan jangan sertakan raw request data secara default.

Akses log layak mendapat perhatian yang sama dengan data aplikasi berhak istimewa lainnya. Simpan log di luar public document root, batasi pembacanya, lakukan rotation, dan tentukan retention period berdasarkan kebutuhan operasional serta hukum yang nyata. Tidak ada satu durasi universal yang cocok untuk setiap situs pribadi atau organisasi.

Hal yang tidak diselesaikan handler minimal

Handler jalan terakhir memperbaiki jalur kegagalan akhir, tetapi beberapa batas tetap ada:

  • Handler ini tidak menangani hasil bisnis yang sudah diperkirakan; kondisi itu tetap membutuhkan respons lokal dan spesifik.
  • Handler ini tidak dapat melindungi respons yang sudah terkirim sebagian dan tidak dapat berjalan sebelum bootstrap mencapai pendaftarannya.
  • Handler ini tidak membuktikan bahwa PHP-FPM, web server, atau operating system menyimpan log entry.
  • Handler ini tidak mendeteksi worker yang hang, proses yang dihentikan, disk penuh, atau server yang tidak pernah memanggil PHP.
  • Handler sebaiknya tetap sederhana karena handler yang rumit dapat gagal ketika aplikasi sudah berada dalam keadaan tidak pasti.

OWASP Error Handling Cheat Sheet merumuskan tujuannya dengan baik: kembalikan respons generik untuk error tak terduga sambil merekam detail server-side untuk investigasi. Ini mengurangi kebocoran informasi yang tidak disengaja, tetapi bukan pengganti untuk memperbaiki defect, memvalidasi input, menegakkan authorization, atau memonitor availability.

Verifikasi jalur kegagalan secara sengaja

Jalur error yang belum pernah dijalankan masih berupa asumsi. Uji di lingkungan non-production yang terkontrol, lalu verifikasi konfigurasi production tanpa memunculkan output sensitif. Pemeriksaan yang berguna meliputi:

  1. Picu test exception yang sudah diketahui setelah handler terdaftar.
  2. Pastikan status respons adalah 500, bukan 200.
  3. Pastikan body hanya memuat pesan generik dan correlation ID, tanpa stack trace, path, SQL, atau secret.
  4. Temukan ID yang sama di log server-side yang dituju dan pastikan event dapat dibaca.
  5. Uji kegagalan validasi biasa secara terpisah dan pastikan status 4xx yang dimaksud tetap digunakan.
  6. Simulasikan log permission yang hilang atau kondisi ruang hampir habis di lingkungan aman, lalu amati perilakunya.
  7. Periksa rotation, retention, dan access permission alih-alih menunggu disk atau pembaca tanpa izin mengungkap kekurangannya.

Logging Cheat Sheet secara eksplisit menganjurkan pengujian kegagalan logging, resource exhaustion, ketahanan terhadap injection, dan access control. Ini bagian penanganan error yang kurang menarik, tetapi bagian inilah yang menentukan apakah bukti akan tersedia ketika dibutuhkan.

Tampilkan lebih sedikit secara publik, pelajari lebih banyak secara privat

Penanganan error production yang baik bukanlah keheningan. Ini adalah pemisahan yang disengaja antara dua audiens. Pengunjung menerima status yang jujur, pesan yang tenang, dan referensi yang dapat mereka laporkan. Operator menerima event terbatas di tempat yang terlindungi. Kegagalan yang diperkirakan tetap spesifik; kegagalan tak terduga mencapai batas jalan terakhir yang kecil.

Pola minimal di sini adalah titik awal. Aplikasi nyata mungkin membutuhkan integrasi framework, centralized collection, alert, trace, atau redaction yang lebih kuat. Pertanyaan yang berguna bukan "Berapa banyak detail error yang dapat ditangkap?", melainkan "Detail minimum apa yang memungkinkan kegagalan ini dipahami tanpa menciptakan risiko kebocoran baru?"

References