Router pada chapter sebelumnya sudah mengenali POST /tugas, tetapi tugas baru yang sesungguhnya selalu datang membawa data: judul, tenggat, dan status. Data tersebut berada di request body — bagian pesan HTTP yang bisa berukuran puluhan byte atau puluhan megabyte. Node.js tidak menyodorkan body tersebut dalam bentuk string yang sudah jadi; req adalah sebuah readable stream, sehingga body datang sedikit demi sedikit dan cara kita mengumpulkannya memakai pola stream yang sudah kita kenal dari Section Buffers dan Streams. Pada chapter ini kita kumpulkan potongan body, urai JSON-nya dengan aman, dan terapkan batas ukuran.

Body adalah Stream #

Mengingat kembali perilaku stream: req — object IncomingMessage yang terdokumentasi di API node:http — memancarkan event data untuk setiap potongan (chunk) yang tiba, lalu end saat semua potongan sudah lengkap. Alih-alih memasang listener manual, kita bisa memakai bentuk for await...of yang jauh lebih ringkas — setiap iterasi menghasilkan satu Buffer, kita menampung semuanya di array, lalu merekatkannya dengan Buffer.concat():

js
import http from 'node:http';

const BATAS_BYTE = 64;

async function bacaBody(req) {
  const potongan = [];
  for await (const bagian of req) potongan.push(bagian);
  return Buffer.concat(potongan);
}

const server = http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/tugas') {
    res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ galat: 'tidak ditemukan' }));
    return;
  }

  const panjang = Number(req.headers['content-length'] ?? '0');
  if (panjang > BATAS_BYTE) {
    req.resume();
    res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({
      galat: `body ${panjang} byte melebihi batas ${BATAS_BYTE} byte`,
    }));
    return;
  }

  const penyangga = await bacaBody(req);
  try {
    const data = JSON.parse(penyangga.toString('utf-8'));
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ diterima: data, byte: penyangga.length }));
  } catch {
    res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ galat: 'body bukan JSON yang valid' }));
  }
});

server.listen(0, '127.0.0.1', async () => {
  const port = server.address().port;
  const dasar = `http://127.0.0.1:${port}`;

  const bagus = JSON.stringify({ judul: 'Belajar HTTP', selesai: false });
  const r1 = await fetch(dasar + '/tugas', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: bagus,
  });
  console.log('JSON valid :', r1.status, bagus.length, 'byte ->', await r1.text());

  const r2 = await fetch(dasar + '/tugas', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: 'ini bukan json',
  });
  console.log('JSON rusak :', r2.status, '->', await r2.text());

  const besar = JSON.stringify({ judul: 'x'.repeat(80) });
  const r3 = await fetch(dasar + '/tugas', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: besar,
  });
  console.log('Body besar :', r3.status, '->', await r3.text());

  server.close();
});

Output yang Dihasilkan:

text
JSON valid : 200 40 byte -> {"diterima":{"judul":"Belajar HTTP","selesai":false},"byte":40}
JSON rusak : 400 -> {"galat":"body bukan JSON yang valid"}
Body besar : 413 -> {"galat":"body 92 byte melebihi batas 64 byte"}

Mari kita telaah bagian-bagian utamanya:

  1. Baris 5-9 (async function bacaBody(req)): inti chapter ini. Array potongan menampung setiap Buffer dari for await...of; array dipilih karena ukurannya bisa tumbuh tanpa kita menebak jumlah potongan. Buffer.concat(potongan) kemudian merekatkan semuanya menjadi satu penyangga utuh — pola yang sama dengan yang kita pakai saat membaca stream berkas.
  2. Baris 18 (req.headers['content-length']): sebelum membaca, kita memeriksa header Content-Length yang dikirim klien. Untuk body kecil nilainya pasti akurat, sehingga cocok dipakai sebagai saringan awal; nilai string-nya kita ubah ke angka lewat Number().
  3. Baris 19-26: bila panjang melampaui BATAS_BYTE, server langsung membalas 413 Payload Too Large tanpa membaca body. req.resume() tetap mengosongkan sisa data yang sudah terkirim agar koneksi bisa dipakai ulang secara bersih.
  4. Baris 28-32 (JSON.parse dalam try...catch): penyangga utuh diterjemahkan menjadi string UTF-8, lalu diurai dengan JSON.parse . Bila klien mengirim teks yang bukan JSON, JSON.parse melempar SyntaxError — dan karena sintaks yang salah adalah salah klien, status yang tepat adalah 400 Bad Request, bukan 500.
  5. Baris 43-48 (permintaan pertama): klien mengirim JSON melalui opsi method, headers, dan body pada fetch. Perhatikan bahwa fetch otomatis mengisi Content-Length — terbukti dari angka 40 byte pada output, padahal kita tidak menuliskan header tersebut.
  6. Baris 51-64: dua permintaan berikutnya sengaja menguji jalur gagal — teks yang bukan JSON dan body yang melebihi batas — sehingga ketiga cabang penanganan terlihat hasilnya berdampingan.

Content-Length dan Batas Ukuran #

Content-Length adalah janji ukuran dari klien; server yang menerima body besar tanpa batas sama saja dengan membuka pintu bagi denial of service — klien jahat cukup mengirim deklarasi ukuran raksasa lalu mengalirkan data tanpa akhir, dan memori kita menumpuk menunggu end yang tidak pernah datang. Praktik yang sehat menggabungkan dua lapis pemeriksaan: saring Content-Length sejak awal seperti demo di atas, lalu tetap waspadai klien yang tidak jujur — framework seperti Express dan Fastify menerapkan batas serupa (umumnya 100 KB hingga 1 MB tergantung konfigurasi) dengan mekanisme pemantauan yang lebih ketat.

Angka 64 byte pada demo memang kecil secara sengaja — mudah dilewati oleh body uji — namun bentuk pemeriksaannya sama persis dengan yang dipakai server produksi. Perilaku lengkap Buffer.concat beserta seluruh operasi penyangga terdokumentasi di API node:buffer . Setelah melewati saringan ukuran dan penguraian JSON, data dari klien akhirnya berwujud object JavaScript yang siap divalidasi dan disimpan.

Memilih Status untuk Body yang Bermasalah #

Ketiga hasil pada output menunjukkan kontrak status yang rapi: body valid dibalas 200, body yang bukan JSON dibalas 400, dan body terlalu besar dibalas 413. Pemetaan ini penting karena klien kita — terutama klien otomatis — membedakan tindak lanjut dari status: 400 berarti “perbaiki payload-mu”, 413 berarti “kecilkan ukuran atau pisah menjadi beberapa bagian”. Body response galat yang berformat JSON membuat pembedaan tersebut semakin mudah diproses secara program.

Kini server kita sudah bisa menerima data dari klien secara utuh. Satu sisi pertukaran masih belum kita sentuh: kendali penuh atas jawaban yang kita kirim balik — status, header khusus seperti cookie dan pengalihan, hingga pengiriman response secara bertahap. Itu bahasan Chapter Response dan Header.