Setiap aplikasi web — dari servlet paling sederhana sampai framework paling canggih — bercakap dalam satu bahasa: HTTP. Sebelum menulis kode sisi server, kita perlu membaca bahasa tersebut dengan lancar: bagaimana permintaan disusun, bagaimana balasan dibentuk, dan apa arti tiap method serta status code. Chapter HTTP Requests dan Responses di Section networking sudah memakai HTTP sebagai klien; sekarang sudut pandangnya dibalik — kita menempati posisi server yang harus mengurai pesan masuk dan menyusun pesan keluar. Chapter ini membongkar anatomi protokol tersebut lalu mengujinya dengan parser pesan yang kita tulis sendiri.

Model Permintaan dan Balasan #

HTTP bekerja dengan model request-response yang sederhana namun ketat: klien mengirim satu permintaan, server mengirim satu balasan, lalu koneksi selesai dari sisi logika aplikasi. Protokol ini juga stateless — setiap permintaan berdiri sendiri dan server tidak mengingat permintaan sebelumnya. Konsekuensinya penting: segala “ingatan” tentang pengguna harus kita bangun sendiri di atas protokol — topik Chapter Sessions dan Cookies.

Anatomi Pesan HTTP #

Pesan HTTP/1.1 hanyalah teks beraturan. Permintaan dibuka oleh request line berisi method, target (path plus query string), dan versi protokol, diikuti baris-baris header Nama: Nilai, lalu satu baris kosong, dan opsionalnya body:

text Contoh permintaan HTTP/1.1 mentah
GET /produk?kategori=buku&urut=harga HTTP/1.1
Host: tokobudi.example
Accept: application/json

Baris kosong pemisah bukan formalitas: server membacanya sebagai tanda “header selesai, sisanya body”. Balasan memakai struktur serupa dengan pembuka status line: HTTP/1.1 200 OK diikuti header dan body. Dua header yang hampir selalu hadir pada balasan adalah Content-Type (jenis media body) dan Content-Length (panjang body dalam byte).

Method dan Sifatnya #

Method menyatakan maksud permintaan. Dua sifat method menentukan bagaimana kita boleh merancang API:

SifatArtiContoh
Safetidak mengubah keadaan serverGET, HEAD
Idempotentdiulang berapa pun pun, hasil akhirnya samaGET, PUT, DELETE
Tidak keduanyatiap panggilan berpotensi mengubah keadaanPOST, PATCH

Sifat ini bukan hiasan dokumentasi. Peramban mengasumsikan GET aman untuk diulang saat refresh; layer retry pada klien boleh mengulang PUT yang gagal, tetapi mengulang POST yang tidak diketahui statusnya dapat mendata transaksi dua kali. Pilih method yang sifatnya sesuai efek sesungguhnya di server.

Status code melapor dalam lima kelas: 1xx informasi protokol, 2xx sukses (200 OK, 201 Created, 204 No Content), 3xx pengalihan (301, 302, 304 Not Modified), 4xx kesalahan klien (400, 401, 403, 404, 405, 409), dan 5xx kesalahan server (500, 503). Angka pertama menentukan reaksi kasar klien; nomor lengkapnya memperhalus reaksi tersebut.

Menguraikan Pesan HTTP dengan Java #

Cara tercepat memahami format pesan adalah menulis penguraiannya sendiri. PesanHttp mengambil teks permintaan mentah, memecahnya menjadi method, target, header, dan query string, lalu membangun balasan lengkap dengan Content-Length yang dihitung dari byte. Penanda akhir baris \r\n pada balasan dicetak sebagai teks terlihat agar standar HTTP/1.1 yang sesungguhnya tidak tersembunyi:

java PesanHttp.java
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

public class PesanHttp {

    enum Metode {
        GET(true, true), POST(false, false), PUT(false, true),
        DELETE(false, true), PATCH(false, false);

        final boolean aman;
        final boolean idempoten;

        Metode(boolean aman, boolean idempoten) {
            this.aman = aman;
            this.idempoten = idempoten;
        }
    }

    public static void main(String[] args) {
        String permintaanMentah = """
                GET /produk?kategori=buku&urut=harga HTTP/1.1
                Host: tokobudi.example
                Accept: application/json
                User-Agent: klien-belajar/1.0
                """;

        System.out.println("== Pesan permintaan mentah ==");
        System.out.print(permintaanMentah);

        String[] baris = permintaanMentah.split("\n");
        String[] bagian = baris[0].trim().split(" ");
        String metode = bagian[0];
        String target = bagian[1];
        String versi = bagian[2];

        Map<String, String> header = new LinkedHashMap<>();
        for (int i = 1; i < baris.length; i++) {
            if (baris[i].isBlank()) {
                continue;
            }
            int titikDua = baris[i].indexOf(':');
            header.put(baris[i].substring(0, titikDua).trim(),
                    baris[i].substring(titikDua + 1).trim());
        }

        System.out.println("== Hasil penguraian pesan ==");
        System.out.println("Method : " + metode);
        System.out.println("Target : " + target);
        System.out.println("Versi  : " + versi);
        System.out.println("Path   : " + target.substring(0, target.indexOf('?')));
        System.out.println("Query  : " + target.substring(target.indexOf('?') + 1));
        header.forEach((nama, nilai) -> System.out.println("Header : " + nama + " -> " + nilai));

        String body = "{\"total\":2,\"kategori\":\"buku\"}";
        StringBuilder respons = new StringBuilder();
        respons.append("HTTP/1.1 200 OK\r\n");
        respons.append("Content-Type: application/json\r\n");
        respons.append("Content-Length: ")
                .append(body.getBytes(StandardCharsets.UTF_8).length).append("\r\n");
        respons.append("\r\n");
        respons.append(body);
        System.out.println();
        System.out.println("== Pesan respons yang dibangun (penanda \\r\\n ditampilkan eksplisit) ==");
        System.out.print(respons.toString().replace("\r\n", "\\r\\n\n"));
        System.out.println();

        System.out.println();
        System.out.println("== Sifat method HTTP ==");
        System.out.printf("%-7s %-8s %-10s%n", "Method", "Aman?", "Idempoten?");
        for (Metode m : Metode.values()) {
            System.out.printf("%-7s %-8s %-10s%n", m, m.aman, m.idempoten);
        }
    }
}

Output yang Dihasilkan:

text
== Pesan permintaan mentah ==
GET /produk?kategori=buku&urut=harga HTTP/1.1
Host: tokobudi.example
Accept: application/json
User-Agent: klien-belajar/1.0
== Hasil penguraian pesan ==
Method : GET
Target : /produk?kategori=buku&urut=harga
Versi  : HTTP/1.1
Path   : /produk
Query  : kategori=buku&urut=harga
Header : Host -> tokobudi.example
Header : Accept -> application/json
Header : User-Agent -> klien-belajar/1.0

== Pesan respons yang dibangun (penanda \r\n ditampilkan eksplisit) ==
HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 29\r\n
\r\n
{"total":2,"kategori":"buku"}

== Sifat method HTTP ==
Method  Aman?    Idempoten?
GET     true     true      
POST    false    false     
PUT     false    true      
DELETE  false    true      
PATCH   false    false     

Mari kita bedah bagian-bagian penguraian pesannya:

  1. Baris 21-26 (text block permintaanMentah): pesan asli disimpan sebagai text block. Indentasi text block dihapus otomatis oleh compiler sehingga tiap baris pesan dimulai tanpa spasi tambahan.
  2. Baris 31-35 (split("\n") lalu split(" ")): baris pertama adalah request line yang dipecah menjadi tiga bagian — method, target, dan versi. Inilah pekerjaan pertama web container terhadap byte yang masuk dari socket.
  3. Baris 37-45 (loop header): setiap baris berikut dipecah pada tanda titik dua pertama dengan indexOf(':') — bukan split(":") — supaya nilai seperti waktu atau URL yang mengandung titik dua tidak terpotong. LinkedHashMap menjaga urutan header seperti yang dikirim klien.
  4. Baris 51-52 (substring(...)): target dipecah pada tanda tanya menjadi path untuk routing dan query string untuk filter — pemisahan yang akan kita lihat lagi ketika membaca parameter permintaan pada Chapter Request dan Response.
  5. Baris 55-60 (pembangunan balasan): Content-Length dihitung dari body.getBytes(StandardCharsets.UTF_8).length, bukan dari body.length(). Keduanya hanya sama bila seluruh karakter berukuran satu byte — ketergantungan pada kebetulan itu akan merusak protokol saat karakter multibyte muncul.
  6. Baris 57 dan 61 (\r\n): standar HTTP/1.1 menetapkan pasangan carriage return dan line feed sebagai penutup baris, bukan \n saja. Baris 65 menampilkan penanda tersebut sebagai teks eksplisit — baris \r\n yang berdiri sendiri pada output adalah baris kosong pemisah header dan body.

Dengan anatomi pesan di tangan, pertanyaan berikutnya: siapa yang menerima pesan mentah ini di sisi server dan mengubahnya menjadi objek yang bisa kita olah? Jawabannya adalah servlet — komponen yang hidup di dalam web container, topik Chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PesanHttp.java

Unduh berkas
java
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

public class PesanHttp {

    enum Metode {
        GET(true, true), POST(false, false), PUT(false, true),
        DELETE(false, true), PATCH(false, false);

        final boolean aman;
        final boolean idempoten;

        Metode(boolean aman, boolean idempoten) {
            this.aman = aman;
            this.idempoten = idempoten;
        }
    }

    public static void main(String[] args) {
        String permintaanMentah = """
                GET /produk?kategori=buku&urut=harga HTTP/1.1
                Host: tokobudi.example
                Accept: application/json
                User-Agent: klien-belajar/1.0
                """;

        System.out.println("== Pesan permintaan mentah ==");
        System.out.print(permintaanMentah);

        String[] baris = permintaanMentah.split("\n");
        String[] bagian = baris[0].trim().split(" ");
        String metode = bagian[0];
        String target = bagian[1];
        String versi = bagian[2];

        Map<String, String> header = new LinkedHashMap<>();
        for (int i = 1; i < baris.length; i++) {
            if (baris[i].isBlank()) {
                continue;
            }
            int titikDua = baris[i].indexOf(':');
            header.put(baris[i].substring(0, titikDua).trim(),
                    baris[i].substring(titikDua + 1).trim());
        }

        System.out.println("== Hasil penguraian pesan ==");
        System.out.println("Method : " + metode);
        System.out.println("Target : " + target);
        System.out.println("Versi  : " + versi);
        System.out.println("Path   : " + target.substring(0, target.indexOf('?')));
        System.out.println("Query  : " + target.substring(target.indexOf('?') + 1));
        header.forEach((nama, nilai) -> System.out.println("Header : " + nama + " -> " + nilai));

        String body = "{\"total\":2,\"kategori\":\"buku\"}";
        StringBuilder respons = new StringBuilder();
        respons.append("HTTP/1.1 200 OK\r\n");
        respons.append("Content-Type: application/json\r\n");
        respons.append("Content-Length: ")
                .append(body.getBytes(StandardCharsets.UTF_8).length).append("\r\n");
        respons.append("\r\n");
        respons.append(body);
        System.out.println();
        System.out.println("== Pesan respons yang dibangun (penanda \\r\\n ditampilkan eksplisit) ==");
        System.out.print(respons.toString().replace("\r\n", "\\r\\n\n"));
        System.out.println();

        System.out.println();
        System.out.println("== Sifat method HTTP ==");
        System.out.printf("%-7s %-8s %-10s%n", "Method", "Aman?", "Idempoten?");
        for (Metode m : Metode.values()) {
            System.out.printf("%-7s %-8s %-10s%n", m, m.aman, m.idempoten);
        }
    }
}