Directive no-cache pada chapter sebelumnya menuntut satu mekanisme agar bermakna: cara bagi cache untuk bertanya “masih segar tidak?” tanpa mengunduh ulang seluruh isi. Mekanisme itulah conditional request — permintaan bersyarat yang membawa penanda versi dari salinan yang sudah dimiliki klien, dan server cukup menjawab 304 Not Modified tanpa body bila isinya belum berubah. Penanda versi yang paling andal adalah ETag: string pendek yang dihitung dari isi respons. Chapter ini membahas cara membangkitkan ETag di PHP, menangani header If-None-Match dan If-Modified-Since, perbedaan validator kuat dan lemah, berapa besar penghematan yang sebenarnya didapat, sampai jebakan ETag yang tidak berubah saat representasi berubah.
ETag sebagai Sidik Jari Konten #
ETag hanyalah string yang Anda pilih — nilainya tidak punya format wajib — tetapi praktik terbaiknya adalah hash dari isi respons, dibungkus tanda kutip. Hash yang sama berarti isi yang sama persis; isi berubah, hash ikut berubah:
<?php
function etagDari(string $konten): string
{
return '"' . hash("xxh128", $konten) . '"';
}
$katalog = '{"produk":"Kopi Torabika","harga":45000}';
$katalogRevisi = '{"produk":"Kopi Torabika","harga":47500}';
echo "representasi awal : " . etagDari($katalog) . "\n";
echo "representasi revisi : " . etagDari($katalogRevisi) . "\n";Output yang Dihasilkan:
representasi awal : "22022b68fbea1fe33aa404a6c89e93d0"
representasi revisi : "e931b10a847d927695facbacf894e03d"Algoritma xxh128 menghasilkan 32 karakter heksadesimal yang cepat dihitung dan cukup untuk keperluan sidik jari konten. Terlihat pada output bahwa perubahan satu angka harga saja menghasilkan ETag yang sepenuhnya berbeda — persis perilaku yang kita inginkan: klien yang memegang ETag lama akan dianggap memegang salinan basi. Untuk konten berbasis berkas, sumber ETag yang lebih murah adalah kombinasi filesize() plus filemtime(), karena keduanya cukup mewakili versi berkas tanpa membaca isinya.
Alur If-None-Match Menuju 304 #
Siklusnya berjalan dua putaran. Putaran pertama: klien meminta konten, server menjawab 200 lengkap dengan header ETag. Putaran kedua: klien menyimpan ETag itu dan mengirimkannya kembali lewat header If-None-Match; server membandingkannya dengan ETag versi terbaru — bila sama, cukup jawab 304 tanpa body. Seluruh keputusan itu bisa dibakukan dalam fungsi murni:
<?php
function etagCocok(?string $ifNoneMatch, string $etag): bool
{
if ($ifNoneMatch === null) {
return false;
}
if (trim($ifNoneMatch) === "*") {
return true;
}
foreach (explode(",", $ifNoneMatch) as $kandidat) {
if (trim($kandidat) === $etag) {
return true;
}
}
return false;
}
function tanganiGet(string $konten, ?string $ifNoneMatch): array
{
$etag = '"' . hash("xxh128", $konten) . '"';
if (etagCocok($ifNoneMatch, $etag)) {
return [304, ["ETag: $etag"], ""];
}
return [200, ["ETag: $etag", "Content-Type: application/json"], $konten];
}
$katalog = '{"produk":"Kopi Torabika","harga":45000}';
$etagKlien = '"' . hash("xxh128", $katalog) . '"';
[$status, $headers, $body] = tanganiGet($katalog, null);
printf("permintaan 1: status=%d, body %d byte, %s\n", $status, strlen($body), $headers[0]);
[$status, $headers, $body] = tanganiGet($katalog, $etagKlien);
printf("permintaan 2: status=%d, body %d byte, %s\n", $status, strlen($body), $headers[0]);Output yang Dihasilkan:
permintaan 1: status=200, body 40 byte, ETag: "22022b68fbea1fe33aa404a6c89e93d0"
permintaan 2: status=304, body 0 byte, ETag: "22022b68fbea1fe33aa404a6c89e93d0"Mari kita bedah dua fungsinya:
etagCocok()menangani tiga bentuk headerIf-None-Match:null(klien belum punya ETag),"*"(cocok dengan apa pun), dan daftar ETag dipisah koma — cache kadang menyimpan beberapa versi sekaligus dan mengirim semuanya, sehingga pembandingan harus per kandidat dengantrim()membersihkan spasi di antaranya.tanganiGet()selalu menghitung ETag versi terbaru lebih dulu — inilah harga yang harus dibayar: server tetap harus tahu isi saat ini sebelum bisa memutuskan jawab200atau304.- Respons
304tetap membawaETag(agar klien menyegarkan penandanya) tetapi body-nya kosong — pada output, permintaan kedua tercatatstatus=304, body 0 byte.
If-Modified-Since dan Last-Modified #
Sebelum ETag ada, revalidasi memakai waktu: server mengirim Last-Modified, klien menanyakan ulang lewat If-Modified-Since. Mekanisme ini masih didukung semua klien dan gratis untuk konten berbasis berkas karena filemtime() tersedia tanpa biaya tambahan:
<?php
function tanganiGetDenganWaktu(DateTimeImmutable $terakhirDiubah, ?string $ifModifiedSince): array
{
$lastModified = $terakhirDiubah->setTimezone(new DateTimeZone("UTC"))->format("D, d M Y H:i:s") . " GMT";
if ($ifModifiedSince !== null) {
$batas = DateTimeImmutable::createFromFormat("D, d M Y H:i:s T", $ifModifiedSince);
if ($batas !== false && $terakhirDiubah->getTimestamp() <= $batas->getTimestamp()) {
return [304, [], ""];
}
}
return [200, ["Last-Modified: $lastModified"], "isi halaman"];
}
$diubah = new DateTimeImmutable("2026-01-10 09:00:00", new DateTimeZone("Asia/Jakarta"));
$hasil = tanganiGetDenganWaktu($diubah, "Mon, 12 Jan 2026 03:00:00 GMT");
printf("klien punya versi 12 Januari => status %d\n", $hasil[0]);
$hasil = tanganiGetDenganWaktu($diubah, "Thu, 08 Jan 2026 03:00:00 GMT");
printf("klien punya versi 8 Januari => status %d, %s\n", $hasil[0], $hasil[1][0]);Output yang Dihasilkan:
klien punya versi 12 Januari => status 304
klien punya versi 8 Januari => status 200, Last-Modified: Sat, 10 Jan 2026 02:00:00 GMTPerbandingan memakai getTimestamp() sehingga zona waktu tidak menjadi sumber galat, dan perubahan yang terjadi persis pada detik yang ditanyakan tetap dijawab 304 (operator <=). Klien versi 12 Januari dijawab 304 karena salinannya lebih baru dari perubahan terakhir; klien versi 8 Januari dijawab 200 beserta Last-Modified yang baru. Kekurangan validator waktu: resolusi hanya satu detik, dan dua perubahan dalam satu detik yang sama tidak terdeteksi. Ketika ETag dan Last-Modified dikirim bersamaan, ETag lebih diutamakan karena lebih presisi.
Validator Kuat dan Lemah #
Prefiks W/ di depan ETag menandakan validator lemah: server hanya menjamin isinya setara secara makna, bukan identik byte per byte. Perbandingan lemah mengabaikan prefiks tersebut:
<?php
function validatorCocokLemah(string $a, string $b): bool
{
$bersihkan = fn (string $validator): string =>
str_starts_with($validator, "W/") ? substr($validator, 2) : $validator;
return $bersihkan($a) === $bersihkan($b);
}
$kuat = '"7d3fa6fa"';
$lemah = 'W/"7d3fa6fa"';
var_dump($kuat === $lemah);
var_dump(validatorCocokLemah($kuat, $lemah));Output yang Dihasilkan:
bool(false)
bool(true)Perbandingan string biasa menganggap keduanya berbeda (false) karena prefiksnya memang bagian dari string — tetapi perbandingan lemah mencopot W/ dulu sehingga keduanya dianggap cocok (true). Validator kuat (tanpa W/) berjanji bahwa dua representasi bernilai sama adalah salinan byte yang identik; janji ini wajib bila Anda memakai ETag untuk melanjutkan unduhan yang terputus (range request). Sebagai contoh, dua output JSON yang hanya berbeda urutan key tidak identik secara byte namun setara secara makna — kandidat yang tepat untuk validator lemah.
Menghemat Bandwidth, Bukan Komputasi #
Harapan yang perlu diluruskan: 304 tidak membuat server berhemat tenaga. PHP tetap berjalan penuh — query tetap dieksekusi, ETag tetap dihitung — yang dihemat adalah transfer salinan yang sama berulang kali. Untuk konten yang tidak berubah setiap saat, penghematan transfer ini sangat besar:
<?php
$ukuranBody = 48_000; // byte: satu respons katalog JSON
$ukuran304 = 220; // byte: baris status + header respons 304
$permintaanPerHari = 10_000;
$tanpaValidasi = $ukuranBody * $permintaanPerHari;
$denganValidasi = $ukuranBody * 100 + $ukuran304 * 9_900; // 1% konten berubah per hari
printf("tanpa validasi : %s MB/hari\n", number_format($tanpaValidasi / 1024 / 1024, 1, ",", "."));
printf("dengan 304 : %s MB/hari\n", number_format($denganValidasi / 1024 / 1024, 1, ",", "."));Output yang Dihasilkan:
tanpa validasi : 457,8 MB/hari
dengan 304 : 6,7 MB/hariPada skenario di atas, 457,8 MB/hari menurun menjadi 6,7 MB/hari — angka yang jauh lebih ramah untuk kuota bandwidth maupun pengguna seluler. Bila target penghematan justru di sisi komputasi server, solusinya adalah cache di level aplikasi seperti yang dibahas di section caching dan performance , bukan conditional request.
Jebakan: ETag Harus Berbeda per Representasi #
ETag mengidentifikasi representasi, bukan sekadar “isi secara abstrak”. Satu URL bisa memiliki beberapa representasi — JSON versus XML, versi polos versus terkompresi — dan masing-masing wajib mendapat ETag yang berbeda. Cara paling aman: ikutkan ciri representasi ke dalam bahan hash:
<?php
function etagRepresentasi(string $konten, string $tipeKonten, string $encoding): string
{
return '"' . hash("crc32b", "$tipeKonten|$encoding|$konten") . '"';
}
$json = '{"produk":"Kopi Torabika"}';
echo "JSON tanpa kompresi : " . etagRepresentasi($json, "application/json", "identity") . "\n";
echo "JSON terkompresi : " . etagRepresentasi($json, "application/json", "gzip") . "\n";
echo "XML : " . etagRepresentasi("<produk>Kopi Torabika</produk>", "text/xml", "identity") . "\n";Output yang Dihasilkan:
JSON tanpa kompresi : "eeeb10c4"
JSON terkompresi : "f56378f4"
XML : "a893325a"Menguji di Server Web Sungguhan #
Alur dua putaran di atas bisa disaksikan langsung dengan server bawaan PHP dan curl:
<?php
// katalog-api.php — dari folder berkas ini, jalankan:
// php -S 127.0.0.1:8090
// lalu uji dua putarannya:
// curl -i http://127.0.0.1:8090/katalog-api.php
// curl -i -H 'If-None-Match: "<etag dari respons pertama>"' http://127.0.0.1:8090/katalog-api.php
$konten = '{"produk":"Kopi Torabika","harga":45000}';
$etag = '"' . hash("xxh128", $konten) . '"';
header("ETag: $etag");
header("Content-Type: application/json");
if (($_SERVER["HTTP_IF_NONE_MATCH"] ?? "") === $etag) {
http_response_code(304);
exit;
}
echo $konten;Putaran pertama menjawab HTTP/1.1 200 OK dengan header ETag: "22022b68fbea1fe33aa404a6c89e93d0" dan body JSON 40 byte. Ketika permintaan kedua dikirim dengan header If-None-Match bernilai persis itu, baris status berubah menjadi HTTP/1.1 304 Not Modified, ETag tetap terkirim, dan body kosong — transfer berhenti di header. Header permintaan selalu hadir di $_SERVER dengan prefiks HTTP_ dan nama uppercase (HTTP_IF_NONE_MATCH), mengikuti pola superglobal yang dibahas di Web Fundamentals. Matikan server setelah selesai dengan Ctrl+C atau pkill -f "php -S".
Ringkasan #
ETag adalah hash representasi yang dikirim sebagai header respons dan diuji kembali lewat If-None-Match — kecocokan dijawab 304 tanpa body, menghemat transfer namun bukan komputasi. Alternatif berbasis waktu (Last-Modified + If-Modified-Since) lebih murah untuk berkas namun terbatas resolusi satu detik, sementara validator lemah W/ mengorbankan jaminan identitas byte demi fleksibilitas semantik. Kuncinya disiplin: ETag wajib berbeda antar representasi dan bekerja beriringan dengan Vary. Setelah respons teks aman dikirim ulang lewat 304, kita beralih ke respons yang justru harus diunduh, bukan ditampilkan: file download, topik chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
// contoh.php — ringkasan ETag dan penanganan conditional request
// dalam fungsi murni yang mengembalikan [status, headers, body].
function etagDari(string $konten): string
{
return '"' . hash("xxh128", $konten) . '"';
}
function etagCocok(?string $ifNoneMatch, string $etag): bool
{
if ($ifNoneMatch === null) {
return false;
}
if (trim($ifNoneMatch) === "*") {
return true;
}
foreach (explode(",", $ifNoneMatch) as $kandidat) {
if (trim($kandidat) === $etag) {
return true;
}
}
return false;
}
function tanganiGet(string $konten, ?string $ifNoneMatch): array
{
$etag = etagDari($konten);
if (etagCocok($ifNoneMatch, $etag)) {
return [304, ["ETag: $etag"], ""];
}
return [200, ["ETag: $etag", "Content-Type: application/json"], $konten];
}
function etagRepresentasi(string $konten, string $tipeKonten, string $encoding): string
{
return '"' . hash("crc32b", "$tipeKonten|$encoding|$konten") . '"';
}
$katalog = '{"produk":"Kopi Torabika","harga":45000}';
echo "=== Putaran pertama: klien belum punya ETag ===\n";
[$status, $headers, $body] = tanganiGet($katalog, null);
printf("status=%d, body %d byte, %s\n", $status, strlen($body), $headers[0]);
echo "=== Putaran kedua: If-None-Match cocok ===\n";
[$status, $headers, $body] = tanganiGet($katalog, etagDari($katalog));
printf("status=%d, body %d byte, %s\n", $status, strlen($body), $headers[0]);
echo "=== Putaran kedua: konten sudah berubah ===\n";
$revisi = '{"produk":"Kopi Torabika","harga":47500}';
[$status, $headers, $body] = tanganiGet($revisi, etagDari($katalog));
printf("status=%d, body %d byte, %s\n", $status, strlen($body), $headers[0]);
echo "=== ETag per representasi ===\n";
$json = '{"produk":"Kopi Torabika"}';
echo "JSON tanpa kompresi : " . etagRepresentasi($json, "application/json", "identity") . "\n";
echo "JSON terkompresi : " . etagRepresentasi($json, "application/json", "gzip") . "\n";
echo "XML : " . etagRepresentasi("<produk>Kopi Torabika</produk>", "text/xml", "identity") . "\n";