Baris pertama setiap respons HTTP adalah baris status: HTTP/1.1 404 Not Found memberi tahu klien apa yang terjadi jauh sebelum body dibaca. Angka tersebut adalah kontrak antara aplikasi Anda dan browser, crawler, maupun klien API — dan klien memercayainya untuk mengambil keputusan: menyimpan cache, mengikuti redirect, menampilkan pesan galat, atau mencoba ulang. Teori lengkap keluarga status code 1xx hingga 5xx sudah kita bahas pada chapter HTTP status codes
di Web Fundamentals. Fokus chapter ini adalah sisi praktisnya di PHP: cara mengatur status lewat http_response_code(), memasangkan status dengan body yang sesuai, memilih kode redirect yang benar, dan membangun halaman galat yang tidak membocorkan apa pun.
Mengatur Status dengan http_response_code() #
Fungsi http_response_code() punya dua wajah. Dipanggil dengan argumen angka, fungsi tersebut mengatur status respons; dipanggil tanpa argumen, fungsi tersebut mengembalikan status saat ini:
<?php
http_response_code(404);
$kodePertama = http_response_code();
http_response_code(201);
$kodeKedua = http_response_code();
echo "setelah diatur 404 : $kodePertama\n";
echo "setelah diatur 201 : $kodeKedua\n";Output yang Dihasilkan:
setelah diatur 404 : 404
setelah diatur 201 : 201Kedua pengaturan dilakukan sebelum ada output, lalu nilainya dibaca dan ditampilkan. Terlihat bahwa pemanggilan terakhir selalu menang: status akhir respons adalah 201, menggantikan 404 yang dipasang sebelumnya. Meskipun contoh ini berjalan di CLI, mekanismenya identik dengan SAPI web — sama seperti header() pada chapter sebelumnya, http_response_code() hanya sah sebelum output pertama:
<?php
echo "konten terlanjur terkirim\n";
// Terlambat: memicu Warning "headers already sent" dan status tetap 200
http_response_code(500);Anti-pattern di atas adalah sumber bug yang menjengkelkan: skrip tampak berjalan normal, peringatan mungkin saja tersembunyi, dan klien menerima 200 OK untuk permintaan yang seharusnya 500. Jika arsitektur aplikasi Anda menyusun body lebih dulu, bungkus keluaran dengan output buffering seperti yang kita bahas pada chapter mengirim HTTP headers
agar jendela pengaturan status tetap terbuka.
Pasangkan Status dengan Body #
Status code bukan hiasan — setiap angka mengatur ekspektasi klien terhadap body. Respons 404 seharusnya membawa penjelasan bahwa sumber daya tidak ada, 201 seharusnya membawa representasi sumber daya yang baru dibuat, dan 204 seharusnya benar-benar kosong. Pola yang rapi menyatukan keduanya dalam satu fungsi:
<?php
function responsJson(int $status, array $isi): string
{
http_response_code($status);
return json_encode(
["status" => $status] + $isi,
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
}
echo responsJson(404, ["error" => "Produk #77 tidak ditemukan"]) . "\n";
echo "http_response_code() setelahnya: " . http_response_code() . "\n";Output yang Dihasilkan:
{
"status": 404,
"error": "Produk #77 tidak ditemukan"
}
http_response_code() setelahnya: 404Mari kita bedah fungsinya. Baris http_response_code($status) dipanggil paling awal — sebelum json_encode() menghasilkan apa pun — sehingga pengaturan status selalu sah berapa pun isi arraynya. Operator union (+) menggabungkan kunci "status" dengan isi tambahan tanpa menimpa kunci yang sudah ada, dan flag JSON_UNESCAPED_UNICODE menjaga teks Indonesia tetap terbaca manusia. Setelah fungsi dipanggil, pembacaan http_response_code() tanpa argumen mengonfirmasi statusnya benar-benar 404.
Body yang dikembalikan fungsi seperti ini tinggal di-echo setelahnya — urutannya aman karena pengaturan status sudah selesai di dalam fungsi sebelum output pertama keluar. Untuk 204 No Content, cukup atur statusnya dan jangan mengeluarkan body sama sekali; klien yang baik akan mengabaikan body pada 204 apa pun isinya.
Memilih Kode Redirect #
Lima kode redirect yang populer bukan sinonim — perbedaannya ada pada dua sumbu: kepermanenan dan nasib metode request. Tabel berikut merangkum keduanya:
| Kode | Permanen? | Metode request berikutnya | Kapan dipakai |
|---|---|---|---|
| 301 | ya | umumnya diubah menjadi GET (perilaku historis browser) | pindah domain atau URL selamanya |
| 302 | tidak | umumnya diubah menjadi GET | redirect sementara umum |
| 303 | tidak | selalu GET | usai POST sukses, arahkan ke halaman hasil (PRG) |
| 307 | tidak | dipertahankan persis | sementara, form POST harus tetap dikirim sebagai POST |
| 308 | ya | dipertahankan persis | permanen, metode tidak boleh berubah |
Kesalahan klasik adalah memakai 302 setelah memproses form: karena perilaku “ubah ke GET”-nya hanya kebiasaan historis, tidak semua klien menanganinya sama. 303 adalah pilihan yang didefinisikan tegas untuk pola POST-Redirect-GET, sementara 307 dan 308 menjaga metode tetap utuh. Pemilihannya bisa dibakukan dalam satu fungsi:
<?php
function pilihKodeRedirect(string $skenario): int
{
return match ($skenario) {
"pindah domain permanen" => 301,
"sementara, klien boleh GET" => 302,
"usai POST, lanjut ke GET" => 303,
"sementara, metode dipertahankan" => 307,
"permanen, metode dipertahankan" => 308,
default => throw new InvalidArgumentException("Skenario tidak dikenal: $skenario"),
};
}
foreach (["usai POST, lanjut ke GET", "permanen, metode dipertahankan"] as $skenario) {
printf("%-36s => %d\n", $skenario, pilihKodeRedirect($skenario));
}Output yang Dihasilkan:
usai POST, lanjut ke GET => 303
permanen, metode dipertahankan => 308Ekspresi match memetakan setiap skenario langsung ke angkanya, dan cabang default melempar InvalidArgumentException untuk skenario yang tidak dikenal — kegagalan cepat lebih baik daripada redirect dengan kode salah yang sulit dilacak. Dalam skrip nyata, kode pilihan dipasangkan dengan header Location lalu exit, karena eksekusi setelah redirect seharusnya berhenti:
<?php
// simpan-pesanan.php — setelah POST diproses sukses
http_response_code(303);
header("Location: /pesanan/terakhir");
exit;Tanpa exit, skrip tetap berjalan sampai selesai — respons sudah diarahkan ke halaman lain, tetapi efek samping seperti pengiriman email ganda di baris berikutnya tetap terjadi sebelum eksekusi berhenti. Detail perilaku redirect dari sisi klien dibahas lebih jauh pada chapter redirect responses
.
Halaman Galat 404 dan 500 #
Halaman galat adalah wajah aplikasi saat sedang buruk: pengguna frustasi mencari bantuan, dan penyerang mencoba memetakan sistem. Dua kebutuhan itu bertabrakan — pesan harus membantu pengguna tanpa membocorkan path, query, atau stack trace kepada penyerang. Fungsi pembangkit halaman galat yang aman berpusat pada escaping:
<?php
function halamanError(int $kode, string $pesan): string
{
$pesanAman = htmlspecialchars($pesan, ENT_QUOTES, "UTF-8");
return "<!DOCTYPE html>\n"
. "<html lang=\"id\">\n"
. "<head><meta charset=\"UTF-8\"><title>Galat $kode</title></head>\n"
. "<body>\n"
. "<h1>Galat $kode</h1>\n"
. "<p>$pesanAman</p>\n"
. "<p><a href=\"/\">Kembali ke beranda</a></p>\n"
. "</body>\n</html>";
}
http_response_code(404);
echo halamanError(404, "Halaman <b>promo</b> tidak ditemukan") . "\n";
echo "status akhir: " . http_response_code() . "\n";Output yang Dihasilkan:
<!DOCTYPE html>
<html lang="id">
<head><meta charset="UTF-8"><title>Galat 404</title></head>
<body>
<h1>Galat 404</h1>
<p>Halaman <b>promo</b> tidak ditemukan</p>
<p><a href="/">Kembali ke beranda</a></p>
</body>
</html>
status akhir: 404Perhatikan output-nya: htmlspecialchars() mengubah <b>promo</b> menjadi <b>promo</b> — tag yang menyusup lewat pesan dicetak sebagai teks biasa, bukan dieksekusi sebagai HTML. Kebiasaan escaping setiap nilai yang masuk ke HTML adalah pertahanan XSS dasar yang dibahas tuntas pada chapter output escaping
. Fungsi pembangkit seperti ini bisa dipakai untuk seluruh halaman galat cukup dengan mengganti kode dan pesannya: 500 dengan pesan generik, 403 tanpa membocorkan alasan ditolaknya akses, dan 410 untuk konten yang sengaja dihapus.
Status 500 membutuhkan penanganan khusus karena sering terjadi di luar alur normal — lewat exception yang tidak tertangkap. Penangan terakhir (last-resort handler) memastikan klien tetap menerima 500 alih-alih 200 berisi pesan error mentah:
<?php
set_exception_handler(function (Throwable $e): void {
http_response_code(500);
error_log("Unhandled: " . $e->getMessage()); // catat untuk tim, bukan untuk klien
echo "Terjadi galat internal. Tim kami telah diberi tahu.\n";
});Handler ini mengatur status, mencatat detail asli ke log untuk investigasi, lalu hanya mengirim pesan generik ke klien — pemisahan “log untuk tim, pesan umum untuk pengguna” yang dibahas pada chapter production error handling .
Perilaku lengkap http_response_code() baru terasa di SAPI web. Router sederhana berikut memutuskan status per jalur URL:
<?php
// router-galat.php — jalankan dengan:
// php -S 127.0.0.1:8098 router-galat.php
// uji dengan:
// curl -i http://127.0.0.1:8098/
// curl -i http://127.0.0.1:8098/produk/999
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($path !== "/") {
http_response_code(404);
echo "Produk tidak ditemukan\n";
exit;
}
http_response_code(200);
echo "Beranda Toko Budi\n";Saat diuji dengan curl -i, jalur / menjawab dengan baris status HTTP/1.1 200 OK dan body beranda, sedangkan /produk/999 menjawab HTTP/1.1 404 Not Found dengan body pesan galatnya — baris status di respons benar-benar berubah mengikuti logika aplikasi. Matikan server setelah selesai dengan Ctrl+C atau pkill -f "php -S".
Status di REST API #
Pada API publik, status code bahkan lebih penting daripada pada halaman web karena konsumennya adalah program, bukan manusia — klien API memetakan alurnya langsung dari angka status. Rekap pemakaian yang lazim:
| Kode | Makna di API | Body respons yang cocok |
|---|---|---|
| 200 OK | permintaan sukses | data lengkap |
| 201 Created | sumber daya baru dibuat | data baru + header Location ke URL-nya |
| 204 No Content | sukses tanpa isi | kosong |
| 400 Bad Request | input salah format | detail kesalahan sintaks |
| 401 Unauthorized | kredensial hilang atau salah | pesan + petunjuk autentikasi |
| 403 Forbidden | terautentikasi tetapi dilarang | pesan tanpa rincian hak akses |
| 404 Not Found | sumber daya tidak ada | pesan |
| 409 Conflict | bentrok state (mis. email sudah terpakai) | penjelasan bentrok |
| 422 Unprocessable Content | validasi semantik gagal | daftar field yang bermasalah |
| 429 Too Many Requests | batas lajuan terlampaui | header Retry-After |
Dua kebiasaan yang membuat API konsisten: satu kesalahan selalu satu kode (jangan bergantian 400 dan 422 untuk kasus yang sama), dan bentuk body error seragam — misalnya selalu {"status": ..., "error": ...} seperti fungsi responsJson() kita di awal chapter. Desain respons API lengkap, termasuk versi dan negosiasi konten, dibahas pada section REST API Development
.
Ringkasan #
http_response_code() mengatur dan membaca baris status sebelum output pertama; pemanggilan terakhir menang, dan setter yang terlambat hanya menghasilkan peringatan dengan status yang tidak berubah. Setiap status menuntut pasangan body yang sesuai — pesan pada 404, representasi baru pada 201, dan kosong pada 204 — sementara pemilihan kode redirect mengikuti kepermanenan dan nasib metode request: 303 untuk usai POST, 307/308 saat metode harus dipertahankan. Halaman galat dibangun dengan escaping ketat dan pesan generik, dengan detail aslinya hanya di log. Dengan status dan header yang kita kuasai, komponen respons berikutnya yang menuntut perhatian adalah cookie — mekanisme yang menyimpan state di sisi klien — dibahas pada chapter cookie configuration
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
// contoh.php — ringkasan pengaturan response status code.
// Seluruh pengaturan status dilakukan sebelum output pertama,
// persis urutan yang benar pada skrip web nyata.
function pilihKodeRedirect(string $skenario): int
{
return match ($skenario) {
"pindah domain permanen" => 301,
"sementara, klien boleh GET" => 302,
"usai POST, lanjut ke GET" => 303,
"sementara, metode dipertahankan" => 307,
"permanen, metode dipertahankan" => 308,
default => throw new InvalidArgumentException("Skenario tidak dikenal: $skenario"),
};
}
function responsJson(int $status, array $isi): string
{
http_response_code($status);
return json_encode(
["status" => $status] + $isi,
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
}
function halamanError(int $kode, string $pesan): string
{
$pesanAman = htmlspecialchars($pesan, ENT_QUOTES, "UTF-8");
return "<!DOCTYPE html>\n"
. "<html lang=\"id\">\n"
. "<head><meta charset=\"UTF-8\"><title>Galat $kode</title></head>\n"
. "<body>\n"
. "<h1>Galat $kode</h1>\n"
. "<p>$pesanAman</p>\n"
. "<p><a href=\"/\">Kembali ke beranda</a></p>\n"
. "</body>\n</html>";
}
// Panggil seluruh pengatur status lebih dulu, sebelum output apa pun.
$contohJson = responsJson(422, ["error" => "email tidak valid"]);
$kodeRpg = pilihKodeRedirect("usai POST, lanjut ke GET");
$kodeTetap = pilihKodeRedirect("permanen, metode dipertahankan");
$halaman404 = halamanError(404, "Halaman <b>promo</b> tidak ditemukan");
echo "=== Respons JSON (422) ===\n";
echo $contohJson . "\n";
echo "=== Kode redirect terpilih ===\n";
printf("%-36s => %d\n", "usai POST, lanjut ke GET", $kodeRpg);
printf("%-36s => %d\n", "permanen, metode dipertahankan", $kodeTetap);
echo "=== Halaman galat 404 ===\n";
echo $halaman404 . "\n";
echo "Status terakhir: " . http_response_code() . "\n";