Setiap respons HTTP yang keluar dari aplikasi PHP membawa dua bagian: header metadata di atas dan body di bawahnya. Lewat header inilah Anda memberi tahu browser bahwa keluaran adalah JSON, memerintahkan pindah halaman, menyetel cookie, atau mengatur perilaku cache — pokok bahasan utama section ini. Teori header HTTP itu sendiri sudah kita bedah pada chapter headers
di Web Fundamentals; fokus kita sekarang adalah sisi PHP-nya: cara kerja fungsi header(), jebakan headers already sent yang paling sering menjepit pemula, peran output buffering, dan pola penyusunan header yang bisa diuji tanpa server. Chapter ini menjadi fondasi bagi seluruh chapter berikutnya, dari status code hingga cookie dan caching.
Cara Kerja Fungsi header() #
Fungsi header() menambahkan satu baris header ke respons yang sedang disusun PHP. Argumennya berupa string lengkap berformat Nama: nilai:
<?php
header("Content-Type: application/json; charset=UTF-8");
header("X-Api-Version: 1");
var_dump(headers_sent());
echo "Jumlah header terkumpul: " . count(headers_list()) . "\n";Output yang Dihasilkan:
bool(false)
Jumlah header terkumpul: 0Dua fungsi pembantu ikut terlihat pada contoh di atas. headers_sent() melaporkan bool(false) karena belum ada output yang keluar — header masih boleh ditambahkan. Lalu headers_list() yang seharusnya menampilkan daftar header terkumpul mengembalikan array kosong di CLI: SAPI CLI tidak pernah mengirim header ke mana pun sehingga daftarnya tidak dipelihara. Header baru benar-benar terlihat pada SAPI web — kita akan mengujinya dengan server sungguhan beberapa bagian di bawah.
Perlu dicatat juga bahwa nama header tidak peka kapitalisasi: Content-Type, content-type, dan CONTENT-TYPE dianggap header yang sama, meski konvensi penulisan memakai huruf kapital di tiap kata. Nilai header tetap peka kapitalisasi sebagaimana string biasa.
Header Harus Dikirim Sebelum Output #
Aturan paling penting sekaligus paling sering dilanggar: header() hanya sah selama belum ada output yang terkirim ke klien. Alasannya melekat pada protokol HTTP itu sendiri — header berada di depan body, jadi begitu satu byte body keluar, kereta header sudah berangkat dan tidak bisa lagi ditambah gerbong. Fungsi headers_sent() memperlihatkan batas tersebut:
<?php
echo "baris ini keluar lebih dulu\n";
var_dump(headers_sent());Output yang Dihasilkan:
baris ini keluar lebih dulu
bool(true)Baris output tercetak lebih dulu, lalu headers_sent() mengembalikan bool(true) — sejak output pertama, status respons sudah dikunci. Memanggil header() setelah titik ini memicu peringatan klasik:
<?php
echo "selamat datang\n";
header("X-Galat: terlambat"); // Warning: Cannot modify header information - headers already sent
Peringatan Warning: Cannot modify header information - headers already sent by (output started at ...) itu bukan sekadar gangguan: header X-Galat tidak pernah terkirim, sementara skrip tetap berjalan seolah semuanya normal. Kesalahan seperti ini sering disembunyikan oleh baris kosong di awal berkas, BOM UTF-8, atau echo yang tidak sengaja tertinggal sebelum blok redirect — dan hanya terasa ketika redirect atau download tiba-tiba tidak jalan.
Output Buffering Menunda Titik Kirim #
Output buffering memberi ruang bernapas: selama buffering aktif, hasil echo ditahan di memori alih-alih langsung dikirim, sehingga jendela untuk memanggil header() tetap terbuka. Mari kita amati urutannya:
<?php
ob_start();
echo "output masuk buffer, belum terkirim ke klien\n";
header("X-Contoh: sah, karena output masih ditahan buffer");
var_dump(headers_sent());
$isiBuffer = ob_get_clean();
echo $isiBuffer;Output yang Dihasilkan:
output masuk buffer, belum terkirim ke klien
bool(false)Mari kita bedah urutan eksekusinya. ob_start() mengaktifkan buffer, sehingga echo pada baris berikutnya masuk ke buffer alih-alih keluar sebagai output sesungguhnya. Karena belum ada yang benar-benar terkirim, header() masih sah dan headers_sent() tetap bool(false) — padahal sudah ada perintah cetak sebelumnya. ob_get_clean() lalu mengambil seluruh isi buffer sekaligus mematikan buffering; echo $isiBuffer mengeluarkannya sebagai output sungguhan setelah semua header selesai dipasang.
Perilaku inilah yang membuat banyak aplikasi tetap berjalan meski header() dipanggil di tengah skrip: konfigurasi output_buffering pada banyak hosting mengaktifkan buffer dengan kapasitas tertentu secara bawaan. Jangan mengandalkan keberuntungan ini — buffer bisa penuh dan tiba-tiba terkirim sebelum waktunya, dan perilakunya berbeda antar lingkungan. Kebiasaan yang benar tetap sama: kumpulkan seluruh keputusan header di awal, baru keluarkan body.
Menguji di Server Web Sungguhan #
CLI tidak pernah mengirim header, jadi perilaku sesungguhnya baru terlihat lewat server web. Server bawaan PHP cukup untuk bereksperimen — berikut dua berkas latihannya:
<?php
// demo-header.php — dari folder berkas ini, jalankan:
// php -S 127.0.0.1:8099
// lalu amati header responsnya:
// curl -i http://127.0.0.1:8099/demo-header.php
header("Content-Type: text/plain; charset=UTF-8");
header("X-Uji: pertama");
header("X-Uji: kedua"); // mengganti "pertama"
header("X-Uji: ketiga", false); // menambah di belakang, tidak mengganti
echo "halo dari server\n";<?php
// telat-header.php — uji dengan: curl -i http://127.0.0.1:8099/telat-header.php
echo "output ini terkirim lebih dulu\n";
header("X-Terlambat: tidak akan pernah sampai");Saat kita menjalankan keduanya lewat curl -i, hasilnya sangat menjelaskan. Respons demo-header.php berawali baris status HTTP/1.1 200 OK disusul Content-Type: text/plain; charset=UTF-8, lalu persis dua baris X-Uji: X-Uji: kedua dan X-Uji: ketiga — baris X-Uji: pertama tidak pernah muncul karena panggilan kedua menggantikannya, sementara parameter false pada panggilan ketiga membuatnya ditambahkan di belakang. Respons telat-header.php tetap berstatus 200, body berisi baris output diikuti pesan Warning: Cannot modify header information - headers already sent yang ikut tercetak sebagai HTML, dan header X-Terlambat sama sekali tidak ada di respons. Setelah selesai bereksperimen, matikan server dengan Ctrl+C atau pkill -f "php -S".
Mengganti atau Menambah Header #
Dua parameter tambahan header() mengendalikan perilaku saat header dengan nama sama sudah dipasang. Parameter kedua $replace (bawaan true) mengganti header lama; false menambahkan baris baru dengan nama sama. Pola tambah ini justru yang dibutuhkan Set-Cookie, karena setiap cookie harus menjadi barisnya sendiri:
<?php
header("Set-Cookie: tema=gelap; Path=/");
header("Set-Cookie: bahasa=id; Path=/", false); // dua cookie, bukan satu
Parameter ketiga mengatur status code respons sekaligus — bentuk yang tepat untuk redirect:
<?php
// simpan-pesanan.php — arahkan klien setelah POST berhasil diproses
header("Location: /pesanan/terakhir", true, 303);
exit;Kombinasi header("Location: ...", true, 303) mengirim header Location beserta status 303 See Other dalam satu panggilan, dan exit di baris berikutnya menghentikan eksekusi agar kode selanjutnya — yang seharusnya tidak lagi berjalan setelah redirect — tidak sempat dieksekusi. Pemilihan angka 301, 302, 303, 307, dan 308 punya konsekuensi berbeda pada metode request berikutnya; pembahasan lengkapnya ada di chapter response status codes
.
Karena CLI membuang header yang Anda kirim, perilaku ganti/tambah bisa dipelajari lewat fungsi murni yang meniru semantik header() di atas array biasa:
<?php
function tambahHeader(array $headers, string $nama, string $nilai, bool $ganti = true): array
{
if ($ganti) {
$headers = array_values(array_filter(
$headers,
fn (string $header): bool => strcasecmp(explode(":", $header, 2)[0], $nama) !== 0
));
}
$headers[] = "$nama: $nilai";
return $headers;
}
$headers = [];
$headers = tambahHeader($headers, "Content-Type", "text/html; charset=UTF-8");
$headers = tambahHeader($headers, "X-Tag", "awal");
$headers = tambahHeader($headers, "x-tag", "pengganti");
$headers = tambahHeader($headers, "X-Tag", "tambahan", false);
foreach ($headers as $header) {
echo $header . "\n";
}Output yang Dihasilkan:
Content-Type: text/html; charset=UTF-8
x-tag: pengganti
X-Tag: tambahanMari kita bedah fungsinya:
- Saat
$gantitrue,array_filter()membuang semua header yang namanya sama dengan$nama. Perbandingan memakaistrcasecmp()sehinggax-tagdanX-Tagdianggap sama — persis perilakuheader(). explode(":", $header, 2)memisahkan nama dari sisa baris dengan batas 2, agar nilai header yang mengandung karakter:(misalnya URLhttps://...) tidak terpotong.array_values()merapikan kunci array setelah filter agar indeks kembali berurutan.- Baris baru selalu ditambahkan di akhir — saat
$gantifalse, tidak ada yang dibuang sehingga efeknya adalah menambah baris dengan nama sama, meniru parameter$replace = false.
Pada output terlihat tiga baris: X-Tag: awal lenyap digantikan x-tag: pengganti (nama sama walau kapitalisasi beda), lalu X-Tag: tambahan menyusul di belakangnya karena dipanggil dengan false.
Menghapus Header dengan header_remove() #
Fungsi header_remove() membatalkan header yang sudah dipasang sebelum terkirim. Dipanggil dengan satu nama, fungsi tersebut membuang semua header dengan nama itu (peka-kapital tidak masalah); dipanggil tanpa argumen, fungsi tersebut membuang seluruh header yang dipasang skrip. Versi fungsinya pada pola array kita:
<?php
function hapusHeader(array $headers, ?string $nama = null): array
{
if ($nama === null) {
return [];
}
return array_values(array_filter(
$headers,
fn (string $header): bool => strcasecmp(explode(":", $header, 2)[0], $nama) !== 0
));
}
$headers = [
"Content-Type: text/html; charset=UTF-8",
"X-Tag: pengganti",
"X-Tag: tambahan",
];
echo implode("\n", hapusHeader($headers, "x-tag")) . "\n";
echo "Setelah hapus semua: " . count(hapusHeader($headers)) . " header\n";Output yang Dihasilkan:
Content-Type: text/html; charset=UTF-8
Setelah hapus semua: 0 headerPemanggilan hapusHeader($headers, "x-tag") membuang kedua baris X-Tag sekaligus meski argumennya ditulis huruf kecil, menyisakan hanya Content-Type — sama seperti header_remove("X-Tag") yang menghapus semua kemunculan nama tersebut. Pemanggilan tanpa argumen mengosongkan koleksi, meniru header_remove() polos yang membatalkan semua header sebelumnya. Kegunaan praktisnya misalnya pada endpoint API yang ingin membuang header default framework sebelum memasang setnya sendiri.
Pola Kumpulan Header yang Teruji #
Melihat kedua fungsi di atas, Anda mungkin menyadari pola yang lebih luas: menyusun header sebagai array murni membuat seluruh logika — urutan, penggantian, penambahan, penghapusan — bisa diuji dengan PHPUnit tanpa server, lalu dikirim dengan satu loop header() saat seluruh keputusan sudah final. Pola ini juga memaksa disiplin yang benar: pengiriman terjadi di satu titik, jauh sebelum echo pertama, sehingga peringatan headers already sent praktis mustahil terjadi.
Berkas contoh.php pada bagian bawah halaman merangkum pola lengkapnya: membangun koleksi header (termasuk dua Set-Cookie lewat parameter tambah), mengirim seluruhnya lewat header(), lalu membersihkan satu jenis dengan header_remove() — semuanya sebelum output pertama, persis urutan yang benar pada skrip web nyata.
Ringkasan #
header() memasang baris header respons sebelum output pertama keluar; setelah itu status terkunci dan panggilan berikutnya hanya menghasilkan peringatan tanpa efek. Output buffering memberi ruang dengan menahan echo di memori, parameter $replace dan kode status mengatur perilaku ganti-tambah, dan header_remove() membatalkan header yang belum terkirim. Menyusun header sebagai array murni membuat semua logika itu teruji tanpa server. Dengan header yang sudah kita kendalikan, pertanyaan berikutnya adalah angka di baris status respons — pembahasan itu kita mulai di chapter response status codes
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
// contoh.php — pola kumpulan header yang bisa diuji tanpa server.
// Seluruh header() dan header_remove() dipanggil sebelum output pertama,
// persis urutan yang benar pada skrip web nyata.
function tambahHeader(array $headers, string $nama, string $nilai, bool $ganti = true): array
{
if ($ganti) {
$headers = array_values(array_filter(
$headers,
fn (string $header): bool => strcasecmp(explode(":", $header, 2)[0], $nama) !== 0
));
}
$headers[] = "$nama: $nilai";
return $headers;
}
function hapusHeader(array $headers, ?string $nama = null): array
{
if ($nama === null) {
return [];
}
return array_values(array_filter(
$headers,
fn (string $header): bool => strcasecmp(explode(":", $header, 2)[0], $nama) !== 0
));
}
// 1) Susun seluruh keputusan header sebagai array murni.
$headers = [];
$headers = tambahHeader($headers, "Content-Type", "application/json; charset=UTF-8");
$headers = tambahHeader($headers, "X-Request-Id", "req-001");
$headers = tambahHeader($headers, "X-Request-Id", "req-002"); // mengganti req-001
$headers = tambahHeader($headers, "Set-Cookie", "tema=gelap; Path=/", false);
$headers = tambahHeader($headers, "Set-Cookie", "bahasa=id; Path=/", false);
// 2) Kirim semua header — masih sebelum output apa pun.
foreach ($headers as $header) {
header($header);
}
// 3) Batalkan satu jenis header, juga sebelum output.
header_remove("X-Request-Id");
// 4) Baru setelah selesai, tampilkan ringkasan keputusan di atas.
echo "=== Header terpasang (sebelum header_remove) ===\n";
foreach ($headers as $header) {
echo $header . "\n";
}
echo "=== Setelah header_remove('X-Request-Id') ===\n";
foreach (hapusHeader($headers, "X-Request-Id") as $header) {
echo $header . "\n";
}