Setiap respons HTTP terdiri dari dua bagian: header yang membawa metadata dan body yang membawa data sebenarnya. Header Content-Type adalah metadata terpenting di antara semuanya karena memberi tahu browser atau client lain cara membaca body respons tersebut. Kesalahan kecil pada header ini bisa berakibat aneh: JSON tampil sebagai teks polos, halaman HTML terunduh sebagai berkas, atau karakter Unicode muncul sebagai simbol tak dikenal. Pada chapter ini kita pelajari media type yang umum dipakai di web, cara mengirim Content-Type dari PHP, negosiasi format lewat header Accept, serta pertahanan terhadap perilaku lama bernama content sniffing.
Media Type dalam HTTP #
Media type — disebut juga MIME type — adalah label berformat jenis/subjenis yang menjelaskan format sebuah data, misalnya text/html untuk dokumen HTML atau application/json untuk data JSON. Label ini terdaftar secara resmi di IANA, dan HTTP memakainya pada header Content-Type untuk respons serta header Accept untuk request. Anatomi header HTTP secara umum sudah kita bahas pada Chapter Headers
; fokus kita sekarang adalah header yang menjelaskan format data. PHP tidak memvalidasi isi header tersebut — fungsi header() mengirim string apa adanya — sehingga ketepatan media type menjadi tanggung jawab kita sebagai penulis kode. Tabel berikut merangkum media type yang paling sering berhubungan dengan PHP:
| Media type | Kegunaan umum | Kaitannya dengan PHP |
|---|---|---|
text/html | Halaman web | Nilai bawaan respons skrip PHP |
application/json | API dan pertukaran data terstruktur | Hasil json_encode() |
application/x-www-form-urlencoded | Kiriman form HTML biasa | Sumber isi $_POST |
multipart/form-data | Form dengan unggahan berkas | Sumber isi $_FILES |
text/plain | Teks polos | Respons debug sederhana |
text/css | Stylesheet | Berkas statis .css |
application/octet-stream | Aliran byte biner | Memaksa unduhan berkas |
Dua media type form pada tabel di atas dibahas tuntas pada bagian khusus di bawah. Sebelum sampai ke sana, kita mulai dari pola yang paling sering Anda tulis di dunia nyata: endpoint JSON.
Mengirim Content-Type dari PHP #
Bila skrip tidak mengirim Content-Type sendiri, PHP menyediakan nilai bawaan text/html — itulah sebabnya halaman PHP langsung tampil rapi di browser tanpa konfigurasi tambahan. Fungsi header() adalah alat untuk mengganti nilai bawaan tersebut, dengan satu aturan keras: pemanggilan harus terjadi sebelum keluaran apa pun terkirim, karena header HTTP selalu berangkat lebih dulu daripada body. PHP bahkan memperingatkan Warning: Cannot modify header information - headers already sent bila aturan ini dilanggar — peringatan yang bisa kita amati langsung bahkan dari CLI. Pola endpoint JSON yang lengkap terlihat seperti ini:
<?php
function kirimJson(array $data, int $kodeStatus = 200): void
{
http_response_code($kodeStatus);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
}
kirimJson(['nama' => 'Budi', 'peran' => 'admin']);Output yang Dihasilkan:
{"nama":"Budi","peran":"admin"}Mari kita bedah fungsinya baris per baris:
- Baris 3 (
http_response_code(...)): menetapkan kode status pada status line respons, misalnya200untuk sukses atau201untuk sumber daya baru. - Baris 4 (
header('Content-Type: application/json; charset=utf-8')): mengirim headerContent-Typesebelum satu byte body pun keluar — urutan inilah yang membuat pola ini sah. - Baris 5 (
json_encode(...)): mengubah array PHP menjadi string JSON. FlagJSON_UNESCAPED_UNICODEmenjaga karakter non-ASCII tetap tercetak apa adanya, bukan menjadi deretan\uXXXX. - Baris 9: pemanggilan demonstrasi; baris baru di akhir hanya untuk keterbacaan saat diuji dari CLI.
Untuk melihat header tersebut benar-benar terkirim, kami menjalankan endpoint yang sama lewat built-in web server lalu mengamatinya dengan curl -i:
<?php
// api.php — jalankan lewat server: php -S 127.0.0.1:8099
// lalu uji dengan: curl -i http://127.0.0.1:8099/api.php
header('Content-Type: application/json; charset=utf-8');
http_response_code(201);
echo json_encode(['status' => 'dibuat', 'id' => 42], JSON_UNESCAPED_UNICODE);Hasil pengujian kami: curl -i menerima baris status HTTP/1.1 201 Created, header Content-Type: application/json; charset=utf-8, dan header X-Powered-By: PHP/8.4.23 sebelum body {"status":"dibuat","id":42}. Sebagai pembanding, skrip yang tidak memanggil header() sama sekali menerima nilai bawaan Content-type: text/html; charset=UTF-8. Perbedaan dua header itulah yang menentukan apakah browser merender halaman atau client API mem-parsing JSON.
Charset untuk Teks Multibyte #
Media type dengan jenis text idealnya dilengkapi parameter charset, misalnya text/html; charset=utf-8, agar penerima tahu bagaimana byte harus didekode menjadi karakter. Tanpa charset yang benar, teks berbahasa Indonesia yang mengandung karakter khusus atau emoji berisiko tampil sebagai deretan simbol aneh — fenomena yang dikenal sebagai mojibake. Sejak PHP 5.6, konfigurasi default_charset bernilai UTF-8 dan dipakai pada header bawaan yang dibuat PHP; namun ketika kita memanggil header() sendiri, string dikirim apa adanya, jadi biasakan mencantumkan charset secara eksplisit.
Form HTML dan Enctype #
Form HTML mengirim data dengan media type yang ditentukan atribut enctype. Nilai bawaannya adalah application/x-www-form-urlencoded: semua field digabung menjadi format nama=nilai yang kita kenal dari query string, lalu dikirim sebagai body request POST. Untuk mengunggah berkas, enctype wajib diganti ke multipart/form-data karena format inilah yang sanggup membawa berkas biner sebagai bagian terpisah dari data teks.
<!-- Form biasa: enctype bawaan application/x-www-form-urlencoded -->
<form method="post" action="/proses.php">
<input type="text" name="email">
<button type="submit">Kirim</button>
</form>
<!-- Form unggahan berkas: wajib multipart/form-data -->
<form method="post" action="/unggah.php" enctype="multipart/form-data">
<input type="file" name="faktur">
<button type="submit">Unggah</button>
</form>Form pertama mengisi $_POST['email'] di sisi server; form kedua mengisi $_FILES['faktur'] berisi metadata berkas yang diunggah. PHP mem-parsing kedua format tersebut secara otomatis — struktur $_FILES akan kita bedah pada Chapter PHP Superglobals
.
Negosiasi Format dengan Header Accept #
Client boleh mengumumkan format yang dipahaminya lewat header Accept, misalnya browser mengirim text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8. Server bebas mengabaikan atau menghormati header tersebut — perilaku yang disebut content negotiation. Versi paling sederhananya: cari media type pertama pada daftar Accept yang juga didukung aplikasi kita:
<?php
function negosiasiContentType(string $headerAccept): string
{
$yangDidukung = ['application/json', 'text/html'];
foreach (explode(',', $headerAccept) as $bagian) {
$tipe = trim(explode(';', $bagian)[0]);
if (in_array($tipe, $yangDidukung, true)) {
return $tipe;
}
}
return 'text/html';
}
echo negosiasiContentType('application/json, text/html') . "\n";
echo negosiasiContentType('text/html,application/xhtml+xml,application/xml;q=0.9') . "\n";
echo negosiasiContentType('image/avif,image/webp,image/apng') . "\n";Output yang Dihasilkan:
application/json
text/html
text/htmlMari kita telaah logikanya:
- Baris 2: fungsi menerima isi header
Acceptsebagai string biasa, sehingga mudah diuji dari CLI dengan contoh header yang dibekukan. - Baris 5:
explode(',', ...)memecah daftar menjadi entri per media type;explode(';', ...)[0]membuang parameterqprioritas, lalutrim()membersihkan spasi. - Baris 7-9:
in_array(..., true)mencari tipe pertama yang didukung; karenaapplication/jsonditulis lebih dulu oleh client pada baris pertama contoh, JSON dipilih. - Baris 12: bila tidak ada yang cocok, fungsi jatuh kembali ke
text/html— perlakuan wajar untuk client yang tidak spesifik.
Perlu diakui bahwa fungsi ini menyederhanakan: header Accept sungguhan memuat bobot q dan wildcard */*, dan library negosiasi matang memperhitungkan semuanya. Konsepnya tetap sama — client mengajukan permintaan format, server memutuskan.
Memaksa Unduhan dengan application/octet-stream #
Media type application/octet-stream menyatakan “aliran byte biner tanpa makna khusus”. Browser meresponsnya dengan mengunduh body alih-alih mencoba merendernya, sehingga type ini menjadi alat baku untuk memaksa unduhan — termasuk berkas teks seperti CSV yang biasanya justru ditampilkan:
<?php
function siapkanHeaderUnduhan(string $namaBerkas, int $ukuran): void
{
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($namaBerkas) . '"');
header('Content-Length: ' . $ukuran);
}
siapkanHeaderUnduhan('laporan-2026.csv', 2048);
echo "Header unduhan siap untuk berkas laporan-2026.csv\n";Output yang Dihasilkan:
Header unduhan siap untuk berkas laporan-2026.csvTiga header bekerja bersama di sini: Content-Type memberi tahu browser bahwa ini aliran biner, Content-Disposition: attachment memerintahkan unduhan dengan nama berkas usulan, dan Content-Length memberi tahu ukuran data agar indikator unduhan akurat. Fungsi basename() sengaja dipanggil pada nama berkas — fungsi tersebut membuang komponen path, mencegah string seperti ../../rahasia.txt lolos menjadi nama unduhan. Pada kode nyata, argumen $ukuran diisi dari filesize() dan body dikirim dengan readfile().
Bahaya Content Sniffing #
Idealnya browser mempercayai Content-Type yang dikirim server. Browser Internet Explorer versi lama terkenal melakukan content sniffing: memeriksa isi body lalu menebak formatnya sendiri, sehingga teks text/plain yang kebetulan mengandung tag HTML bisa dieksekusi sebagai halaman web — pintu masuk klasik serangan XSS. Browser modern sudah jauh lebih patuh, tetapi web masih menyimpan pertahanan baku untuk mewarisi masalah ini: header X-Content-Type-Options: nosniff melarang browser melakukan sniffing apa pun terhadap respons kita.
<?php
header('Content-Type: text/plain; charset=utf-8');
header('X-Content-Type-Options: nosniff');
echo "Respons ini pasti diperlakukan sebagai teks polos.\n";Output yang Dihasilkan:
Respons ini pasti diperlakukan sebagai teks polos.Kedua header saling menguatkan: Content-Type menyatakan format sebenarnya, lalu nosniff memaksa browser menaatinya tanpa menebak-nebak. Praktik ini murah, tanpa efek samping, dan menjadi standar pada respons teks serta unduhan.
Ringkasan #
Content-Type adalah kontrak format antara server dan client: media type menjelaskan jenis data, parameter charset menjelaskan encoding untuk teks, dan header Accept memungkinkan client ikut menentukan format yang diinginkan. Kita juga telah melihat pola header() sebelum output, endpoint JSON dengan json_encode(), perilaku enctype pada form, serta header nosniff sebagai pertahanan. Kontrak format hanya separuh cerita — alamat tujuan request adalah separuh lainnya, dan itulah topik Chapter URLs dan Query Strings
.