Dua pemula pernah mengirim permintaan POST yang persis sama ke server tiruan yang sama, tetapi hasilnya bertolak belakang: milik Siti diproses menjadi data baru, milik Budi dijawab 400 Bad Request dengan pesan “body tidak dapat diurai”. Bedanya satu header: Content-Type. Server tidak bisa menebak format isi pesan — ia membaca labelnya, dan bila label salah, isi sebaik apa pun jadi tidak terbaca. Chapter ini merapikan pengelolaan header: dari Content-Type dan Accept, header kustom, sampai membaca header respons lewat object Headers.

Berkas contoh kembali menanam server tiruannya sendiri; server mencetak header yang diterima sehingga kita bisa membuktikan apa yang benar-benar dikirim fetch. Jalankan node kelola-header.mjs untuk mereproduksi keluarannya.

Content-Type: Label Format Isi Pesan #

Header Content-Type menjawab satu pertanyaan: “isi pesan ini dalam format apa?”. Untuk API JSON, nilainya application/json — sering dilengkapi charset=utf-8 pada arah respons. Aturan praktisnya:

  • Body JSON dikirim → sertakan Content-Type: application/json di request.
  • Tanpa header itu, banyak server menolak body atau mengurainya sebagai teks mentah.
  • Pada response, Content-Type menjadi dasar keputusan client memilih response.json() atau response.text().

Perhatikan bahwa Content-Type tidak mengubah data — ia hanya deskripsi. Data JSON tetap diserialisasi JSON.stringify seperti biasa; header-lah yang memberi tahu server cara membaca teksnya.

Mengirim Header lewat Options #

Semua header request dikirim lewat properti headers pada options fetch. Berkas berikut mengirim tiga header — format isi, format yang diharapkan, dan satu header kustom — lalu server tiruan mencetak apa yang diterimanya, dan client membaca header respons:

// kelola-header.mjs
// Mengirim header permintaan dan membaca header respons.
// Jalankan di terminal: node kelola-header.mjs (tanpa internet).

import http from "node:http";

const server = http.createServer((req, res) => {
  // Server memeriksa header yang dikirim client.
  console.log("SERVER content-type :", req.headers["content-type"] ?? "(tidak ada)");
  console.log("SERVER accept       :", req.headers["accept"] ?? "(tidak ada)");
  console.log("SERVER x-api-key    :", req.headers["x-api-key"] ?? "(tidak ada)");

  res.writeHead(200, {
    "Content-Type": "application/json; charset=utf-8",
    "X-Server-Waktu": "server-tiruan-v1",
    "Cache-Control": "max-age=60",
  });
  res.end(JSON.stringify({ id: 17, judul: "Kamus Bahasa" }));
});

server.listen(0, "127.0.0.1", async () => {
  const port = server.address().port;

  const response = await fetch(`http://127.0.0.1:${port}/produk/17`, {
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      "X-Api-Key": "kunci-demo-budi-123",
    },
  });

  // response.headers adalah object Headers: baca satuan atau iterasi.
  console.log("CLIENT content-type :", response.headers.get("Content-Type"));
  console.log("CLIENT cache-control:", response.headers.get("Cache-Control"));
  console.log("CLIENT ada x-server-waktu?", response.headers.has("x-server-waktu"));
  console.log("--- semua header respons (urut abjad):");
  for (const [nama, nilai] of response.headers) {
    if (nama === "date") continue; // header date berubah setiap saat, dilewati agar keluaran stabil
    console.log(`${nama}: ${nilai}`);
  }

  server.closeAllConnections();
  server.close();
});

Output yang Dihasilkan:

text
SERVER content-type : application/json
SERVER accept       : application/json
SERVER x-api-key    : kunci-demo-budi-123
CLIENT content-type : application/json; charset=utf-8
CLIENT cache-control: max-age=60
CLIENT ada x-server-waktu? true
--- semua header respons (urut abjad):
cache-control: max-age=60
connection: keep-alive
content-type: application/json; charset=utf-8
keep-alive: timeout=5
transfer-encoding: chunked
x-server-waktu: server-tiruan-v1

Mari kita telaah baris-baris pentingnya:

  1. Baris 9–11 (server membaca header): server mengakses req.headers["content-type"] dengan nama huruf kecil. HTTP header tidak peka huruf besar-kecil, dan Node menyeragamkan semuanya ke huruf kecil di object req.headers — inilah bukti bahwa Content-Type yang dikirim fetch benar-benar tiba.
  2. Baris 25–29 (options headers): tiga header dikirim. Content-Type mendeskripsikan body, Accept menyatakan “saya hanya menerima JSON”, dan X-Api-Key adalah header kustom — konvensi X- awalan yang lazim untuk metadata aplikasi. Nilainya fiktif; pada server sungguhan, kredensial semacam ini dijaga rahasia dan tidak pernah ditulis di kode.
  3. Baris 33 (headers.get("Content-Type")): get juga tidak peka huruf besar-kecil — menulis "content-type" memberi hasil yang sama.
  4. Baris 35 (headers.has(...)): has memeriksa keberadaan header tanpa perlu nilainya; berguna untuk header yang nilai jarang dibaca.
  5. Baris 37–40 (iterasi header respons): object Headers bisa diiterasi seperti map, dan hasilnya terurut abjad menurut nama — bukan urutan kiriman server. Baris continue melewati date karena isinya berubah setiap saat dan akan membuat keluaran contoh tidak stabil.
  6. Keluaran bagian bawah: selain dua header yang kita set di writeHead, fetch menampilkan header teknis bawaan protokol — connection, keep-alive, transfer-encoding — yang diurus Node tanpa kita campuri.

Accept dan Negosiasi Format #

Header Accept mengumumkan format yang bersedia diterima client. Server yang patuh akan menjawab dalam format itu — atau dengan 406 Not Acceptable bila tidak mampu. Dalam praktik API JSON modern, Accept: application/json sering bersifat dokumentatif karena hampir semua endpoint memang menyerahkan JSON; header ini justru krusial saat satu endpoint mampu melayani banyak format, misalnya text/html untuk browser dan application/json untuk skrip.

Pola negosiasi dua arah yang seimbang terlihat dari keluaran berkas contoh: request mengumumkan Accept: application/json, response mengabulkan lewat Content-Type: application/json; charset=utf-8.

Membaca Response Headers sebagai Object Headers #

response.headers adalah instance Headers — interface standar dengan tiga cara membaca:

  • headers.get("nama") — nilai satu header, atau null bila tidak ada.
  • headers.has("nama") — boolean keberadaan.
  • iterasi for (const [nama, nilai] of headers) atau headers.forEach(...) — seluruh pasangan, terurut abjad.

Untuk keputusan parsing, pola yang tangguh memeriksa Content-Type sebelum memilih pembaca body:

js
const response = await fetch(url);
const tipe = response.headers.get("content-type") ?? "";

if (tipe.includes("application/json")) {
  const data = await response.json();
  console.log("JSON:", data);
} else {
  const teks = await response.text();
  console.log("bukan JSON:", teks.slice(0, 40));
}

Polanya memanfaatkan includes karena Content-Type sering menyertakan parameter tambahan seperti charset=utf-8 di belakang tipe utamanya. Cuplikan ini mengikuti berkas contoh: berjalan terhadap server tiruan yang sama, keluarannya JSON: { id: 17, judul: 'Kamus Bahasa' } bila server mengirim JSON.

Header yang fetch Kirim Otomatis #

Bahkan tanpa options headers, fetch tetap melampirkan beberapa header sendiri. Berkas contoh tadi membuktikannya lewat sisi server; isi khasnya:

HeaderNilai di contoh NodePeran
accept*/*Menerima format apa pun
user-agentnodeIdentitas aplikasi pengirim
connectionkeep-aliveKelola pemakaian ulang koneksi
accept-encodinggzip, deflateFormat kompresi yang dipahami

Di browser, daftar ini bertambah dua-duanya: user-agent berisi identitas browser yang panjang, dan sec-fetch-mode: cors menyertai permintaan lintas origin sebagai penanda konteks keamanan. Header otomatis ini bisa ditimpa — menambah Accept: application/json pada options, seperti pada berkas contoh, menggantikan */* bawaan.

Kapan Header Kustom Dipakai #

Header kustom adalah saluran metadata aplikasi yang tak disediakan protokol. Tiga kebiasaan industri yang layak ditiru:

  • Identitas permintaan — server diberi X-Request-Id dari client, lalu mencantumkannya di log; saat galat dilaporkan pengguna, id itu memudahkan pencarian jejak permintaan di sisi server.
  • Penanda integrasi — X-Api-Key untuk mengenali aplikasi pemanggil, sebagaimana disimulasikan berkas contoh.
  • Informasi balik — server pun boleh berekspresi lewat header kustom, misalnya X-RateLimit-Remaining: 42 yang memberi tahu sisa kuota permintaan sebelum 429 mengetuk.

Pemakaian header kustom pada permintaan lintas origin punya konsekuensi tambahan: browser menuntut header itu terdaftar di Access-Control-Allow-Headers respons preflight — persis seperti yang dilakukan server tiruan di berkas contoh untuk X-Api-Key.

Header bukan sekadar label format — sebagian header menentukan keamanan percakapan lintas origin. Itulah topik chapter CORS berikutnya.