Halaman error yang memperlihatkan stack trace, path file, dan nilai environment adalah alat debugging terbaik di komputer kita — dan kebocoran data terburuk di server publik. Satu variabel .env mengatur perbedaan kedua dunia itu: APP_DEBUG. Pada chapter ini kita jalankan exception yang sama dua kali, sekali dengan debug aktif dan sekali dimatikan, lalu bandingkan hasilnya byte demi byte. Kita juga pelajari cara meniru perilaku produksi di mesin lokal dan menguji kedua mode lewat feature test — karena perbedaan ini terlalu penting untuk dibiarkan pada kebiasaan.
Perbandingan Nyata: Mode yang Sama, Hasil yang Berbeda #
Route uji kita tetap route /crash dari chapter Debugging Laravel
. Pertama, APP_DEBUG=true — inilah yang pengguna lokal lihat: halaman 881.467 byte berisi class exception, pesan, stack trace interaktif, potongan kode, dan environment. Kedua, APP_DEBUG=false:
Output yang Dihasilkan (request HTML yang sama, debug dimatikan):
status: 500
panjang_body: 6265 bytes
title: Server Error
ada_pesan_exception: false
ada_stack_trace: false
ada_path_file: falseHalaman menyusut dari 881.467 byte menjadi 6.265 byte. Pesan “Gagal menyambung ke layanan pengiriman.” tidak pernah dikirim, tidak ada nama file, tidak ada baris, tidak ada stack trace — hanya halaman generik “Server Error”. Perbedaan yang sama terlihat pada JSON untuk request API:
Output yang Dihasilkan (config(['app.debug' => false]) lalu getJson('/crash')):
status: 500
body_json: {
"message": "Server Error"
}Saat debug aktif, response JSON yang sama menyertakan exception (nama class), message, file, line, dan array trace — data yang memang dibutuhkan konsumen API internal saat development, tetapi berbahaya di publik.
Apa yang Bocor saat Debug Aktif #
Daftar di atas bukan sekadar “kurang sopan” — itu materi kebocoran:
- Path absolut server (
/home/z/.../routes/web.php) membantu penyerang memetakan struktur deployment. - Stack trace membocorkan nama package, versi, dan alur internal aplikasi.
- Environment & detail request bisa memuat isi
$_ENV— termasuk kredensial database dan API key bila ter-log.
Production 500 dan Korelasi Log #
Jika pengguna produksi hanya melihat “Server Error”, dari mana kita tahu apa yang terjadi? Dari log — pelaporan exception tetap berjalan penuh terlepas dari APP_DEBUG, karena pipeline report() independen dari pipeline render. Feature test kita membuktikannya:
config()->set('app.debug', false);
$response = $this->getJson('/crash');
fwrite(STDERR, 'status: ' . $response->status() . "\n");
$log = trim((string) File::get(storage_path('logs/laravel.log')));
fwrite(STDERR, "log_baris_pertama:\n" . strtok($log, "\n") . "\n");Output yang Dihasilkan:
status: 500
body_json: {
"message": "Server Error"
}
log_baris_pertama:
[2026-10-13 19:56:39] testing.ERROR: Gagal menyambung ke layanan pengiriman. {"request_id":"req-tanpa-id","user_id":null,"exception":"[object] (RuntimeException(code: 0): Gagal menyambung ke layanan pengiriman. at .../routes/web.php:62)Pengguna menerima “Server Error”; log menyimpan class exception, pesan, file, baris, dan stack trace penuh — ditambah request_id dari middleware kita. Pola kerjanya jelas: tunjukkan yang generik ke pengguna, simpan yang detail di log, lalu korelasikan keduanya lewat request id atau timestamp. Bila produk punya halaman error 500 kustom, itu ditempatkan di resources/views/errors/500.blade.php seperti pada chapter Custom Error Pages
.
Meniru Produksi Secara Lokal #
Kita tidak perlu deploy untuk mengetahui nasib error di produksi. Ubah environment lokal sesaat:
APP_ENV=production
APP_DEBUG=falseDengan konfigurasi itu, request /crash di mesin kita membalas persis seperti server: halaman “Server Error” polos, log tetap detail. Perubahan APP_ENV juga ikut terlihat di label baris log yang berubah menjadi production.ERROR (bukan local.ERROR) seperti dijelaskan chapter Logging Configuration
. Ingat pula aturan config:cache — setelah php artisan config:cache, framework membaca konfigurasi dari cache dan perubahan .env tidak berlaku sampai cache dihapus (config:clear).
Menguji Kedua Mode dalam Satu Suite #
Karena app.debug dibaca dari config saat render, feature test bisa membaliknya per-scenario. Test berikut memastikan mode produksi benar-benar menutup kebocoran:
public function test_debug_false_json_500(): void
{
config()->set('app.debug', false);
$response = $this->getJson('/crash');
fwrite(STDERR, 'status: ' . $response->status() . "\n");
fwrite(STDERR, 'body_json: ' . $response->getContent() . "\n");
$this->assertSame(500, $response->status());
$this->assertSame('Server Error', $response->json('message'));
}
public function test_debug_true_menyertakan_detail(): void
{
config()->set('app.debug', true);
$response = $this->getJson('/crash');
$json = $response->json();
fwrite(STDERR, 'exception_class: ' . ($json['exception'] ?? '-') . "\n");
fwrite(STDERR, 'ada_file_baris: ' . var_export(isset($json['file'], $json['line']), true) . "\n");
fwrite(STDERR, 'ada_trace: ' . var_export(isset($json['trace']) && count($json['trace']) > 0, true) . "\n");
$this->assertSame(500, $response->status());
$this->assertSame('RuntimeException', $response->json('exception'));
}Output yang Dihasilkan:
status: 500
body_json: {
"message": "Server Error"
}
exception_class: RuntimeException
ada_file_baris: true
ada_trace: truePasangan test semacam ini menjadi penjaga ber pagar: jika ada saatnya seseorang tidak sengaja menyalakan debug di produksi, suite inilah yang teriak duluan. Topik penyertaan APP_DEBUG di daftar variabel deployment tersedia di dokumentasi Configuration
. Berikutnya kita angkat sudut pandang operator: begitu aplikasi berjalan di produksi, bagaimana kita tahu error datang tanpa menunggu keluhan pengguna? Itu bahasan chapter Monitoring Application Errors
.