HTTP, protokol yang dipakai server Anda.
Penjelasan dari prinsip dasar tentang protokol lapisan aplikasi yang dipakai setiap backend: dari statusless dan semantik method, sampai caching, conditional request, proxy, dan TLS. Ditulis bukan hanya untuk menyebut fungsi tiap bagian, tapi mengapa ia ada dan bagaimana ia bekerja di dalamnya. Ada implementasi Go dan Python, berpijak pada MDN dan RFC 9110.
Prinsip Inti HTTP
HTTP (HyperText Transfer Protocol) adalah protokol lapisan aplikasi: Lapisan 7 model OSI, untuk mengirim hipermedia dan, yang lebih penting bagi kita, untuk akses terprogram ke API dan komunikasi antar-mesin. "Lapisan aplikasi" berarti ini lapisan teratas, yang mendefinisikan makna sebuah pesan (ini GET untuk /users/5), bukan bagaimana byte bergerak secara fisik di kabel. Sebagai engineer backend kita hampir seluruhnya hidup di sini; lapisan di bawahnya (segmen TCP, paket IP, frame Ethernet) diabstraksikan sebagai "pipa yang bisa dipercaya." Dua gagasan ada di jantung HTTP.
1. Statelessness
Server tidak menyimpan memori sesi antar dua request. Setiap request sepenuhnya mandiri: ia harus membawa semua yang dibutuhkan server untuk memprosesnya, token auth, cookie, resource target, method, preferensi apa pun. Begitu selesai merespons, server membuang semua pengetahuan tentang request tersebut. Request kedua dari klien yang sama satu milidetik kemudian diperlakukan sebagai peristiwa baru tanpa hubungan apa pun. Protokol ini amnesia secara desain.
Mengapa statelessness adalah fitur yang disengaja, bukan keterbatasan
Server yang "mengingat" Anda terdengar lebih sederhana. Statelessness dipilih justru karena melupakan memberi tiga sifat yang sangat berarti saat skala besar:
- Simpel: server tidak mengalokasikan, melacak, menghentikan, atau membersihkan state sesi per pengguna di memori. Setiap request ditangani terpisah, jadi logika handler adalah fungsi murni dari inputnya. Sedikit bagian yang bergerak berarti sedikit bug.
- Skalabilitas horizontal: karena tidak ada server yang "memiliki" percakapan, request apa pun bisa diarahkan ke server mana pun dalam satu armada. Load balancer bisa menyebar sepuluh request dari satu pengguna ke sepuluh mesin berbeda dan masing-masing menanganinya dengan benar, karena setiap request membawa konteksnya sendiri. Tanpa statelessness Anda butuh "sticky session" yang mengunci tiap pengguna ke satu mesin, boros kapasitas dan rusak saat mesin itu mati.
- Tahan crash: kalau satu node mati di tengah jalan, tidak ada sesi di memori yang perlu dibangun ulang. Klien tinggal mencoba lagi, load balancer mengarahkannya ke tempat lain, tidak ada yang hilang. Server yang menyimpan state, sebaliknya, kehilangan semua sesi aktif saat crash.
Kompensasinya: kontinuitas harus dibangun ulang di tiap request. Karena protokolnya sendiri lupa, kita membangun ulang identitas setiap kali memakai cookie, sesi sisi server, atau token, mengirim kredensial kecil bersama tiap request supaya server bisa mencari tahu siapa Anda. Teknik pengelolaan state itu dirinci di bagian 8. Intinya di sini: semuanya berada di atas protokol yang stateless, bukan mengubah hakikatnya.
2. Model Klien: Server
Komunikasi selalu dimulai oleh klien: browser, aplikasi mobile, atau layanan backend lain. Klien memutuskan apa yang ia mau, menyusun request lengkap (URL, method, header, body), lalu mengirimnya. Server pada dasarnya pasif: ia menampung resource, mendengarkan, dan hanya berbicara sebagai respons atas request. Dalam HTTP klasik ia tidak bisa mendorong data sendiri ke klien yang belum meminta. Asimetri inilah kenapa "server tidak bisa memberi tahu browser ada pesan baru" dulu diselesaikan dengan polling, dan kenapa WebSocket serta Server-Sent Events (bagian 19, bagian 17) ada untuk keluar dari bentuk request/response yang kaku ketika benar-benar butuh server push.
Transport di bawahnya: kenapa TCP
HTTP tidak menggerakkan byte sendiri; itu diserahkan ke protokol transport. HTTP punya satu syarat keras untuk transport itu: keandalan. Pesan harus tiba utuh dan berurutan, atau transport harus menandai error. HTTP tidak tahan byte yang diam-diam hilang atau tertukar urutannya, karena header atau body yang setengah jalan tidak ada artinya. Dua transport dominan di internet adalah TCP dan UDP:
- TCP berbasis koneksi dan andal: ia melakukan 3-way handshake (SYN -> SYN-ACK -> ACK) untuk membangun koneksi, memberi nomor pada tiap byte, mengonfirmasi penerimaan, mengirim ulang yang hilang, dan mengantarkan data secara berurutan. Ini yang dipakai HTTP/1 dan HTTP/2.
- UDP tanpa koneksi dan "kirim lalu lupakan", cepat tapi tanpa jaminan pengiriman. HTTP mentah tidak bisa memakainya langsung, tetapi lapisan QUIC pada HTTP/3 membangun ulang keandalan di atas UDP (bagian 2).
Handshake TCP dan negosiasi TLS terjadi di bawah Lapisan 7, itu urusan rekayasa transport atau jaringan. Kita perlakukan keduanya sebagai: "pipa yang andal dan mungkin terenkripsi sudah terbentuk, sekarang pesan request dan response mengalir di atasnya." Abstraksi itu cukup untuk menalar dan men-debug sebagian besar perilaku backend.
Tiga sifat yang mengklasifikasikan setiap method
Tiga istilah presisi dari spesifikasi ini muncul berulang di seluruh manual dan mengatur cara method berperilaku, cara cache memperlakukannya, dan apakah klien boleh mengulangnya. Dipreview di sini dan dibedah di bagian 6:
- Safe: request hanya membaca; menjalankannya tidak mengubah state server (mis. GET, HEAD). Crawler dan prefetcher tautan mengandalkan ini.
- Idempotent: menjalankan request sekali atau N kali meninggalkan server dalam state akhir yang sama (mis. GET, PUT, DELETE). Inilah sifat yang membuat request aman diulang setelah timeout.
- Cacheable: respons boleh disimpan dan dipakai ulang untuk memenuhi request setara yang datang belakangan (mis. GET, HEAD), menghemat satu round trip.
Evolusi HTTP
Semua versi HTTP mempertahankan semantik request/response yang sama, tetapi mengubah cara koneksi yang mendasarinya dipakai, masing-masing memburu latensi yang lebih rendah. Memahami urutannya menjelaskan kenapa masalah performa tertentu ada dan bagaimana HTTP modern menyelesaikannya.
| Versi | Tahun | Perubahan kunci & perilaku koneksi |
|---|---|---|
| HTTP/0.9 | 1991 | Protokol satu baris: GET /page dan responsnya HTML mentah. Tanpa header, tanpa status code, tanpa method lain, tanpa metadata apa pun. |
| HTTP/1.0 | 1996 | Menambahkan header, status code, POST, dan tipe konten, struktur yang masih kita pakai. Tapi ia membuka koneksi TCP baru untuk setiap request dan menutupnya setelah respons, membayar biaya handshake penuh tiap kali. |
| HTTP/1.1 | 1997 | Koneksi persisten (keep-alive) sebagai bawaan, pipelining, chunked transfer encoding, header Host yang wajib, dan caching yang lebih kaya. Versi dominan selama dua dekade. |
| HTTP/2 | 2015 | Multiplexing, binary framing, kompresi header (HPACK), dan server push. |
| HTTP/3 | 2022 | Meninggalkan TCP dan memakai QUIC di atas UDP. |
Masalah yang diselesaikan tiap versi
Pemborosan 1.0. Membangun koneksi TCP butuh satu round trip (handshake) sebelum satu byte HTTP mengalir. Melakukannya untuk tiap gambar, skrip, dan stylesheet di halaman, puluhan koneksi, menambah latensi besar. Koneksi persisten 1.1 memperbaikinya dengan memakai ulang satu koneksi TCP untuk banyak pasangan request/response (bagian 16), mengamortisasi handshake.
Header Host. 1.1 mewajibkan Host, yang menghidupkan virtual hosting: satu alamat IP dan satu server bisa menampung banyak domain, mengarahkan tiap request berdasarkan header Host-nya. Praktis seluruh dunia shared-hosting dan reverse proxy bergantung pada ini.
Head-of-line blocking. Meski memakai koneksi persisten, HTTP/1.1 memproses request pada satu koneksi secara serial: respons 2 tidak bisa mulai sebelum respons 1 selesai. Satu respons lambat menahan semua yang mengantre di belakangnya. Multiplexing HTTP/2 menyelesaikan ini di lapisan aplikasi: banyak stream independen berbagi satu koneksi, dan frame dari stream yang berbeda disisipkan, jadi respons lambat tidak lagi memblokir yang lain. HTTP/2 juga berpindah dari teks yang panjang lebar ke lapisan binary framing yang ringkas, menambah kompresi header HPACK (header berulang seperti cookie dan user-agent dikirim sekali lalu direferensikan), plus server push (mengirim resource yang akan dibutuhkan klien lebih dulu, kini sebagian besar sudah ditinggalkan demi preload hints).
Head-of-line blocking milik TCP. HTTP/2 menghilangkannya di lapisan HTTP, tapi masalah yang lebih dalam tetap ada: karena semua stream berbagi satu koneksi TCP, satu paket TCP yang hilang menahan semua stream sampai dikirim ulang, sebab TCP menjamin pengiriman berurutan untuk seluruh aliran byte. QUIC pada HTTP/3 memperbaikinya dengan berjalan di atas UDP dan mengimplementasikan keandalan per stream sendiri: paket yang hilang hanya memblokir stream tempatnya berada. QUIC juga menggabungkan handshake TLS ke dalam pembentukan koneksi, jadi koneksi aman terbentuk dalam lebih sedikit round trip (sering nol saat resumption).
Di semua versi, model mentalnya sama: klien dan server membangun koneksi, lalu pesan mengalir bolak-balik. Versi-nya hanya mengoptimalkan pipa: pemakaian ulang koneksi, paralelisme, kompresi, dan transport yang membawanya.
Anatomi Pesan & Tipe MIME
Semua komunikasi HTTP adalah dua pesan teks terstruktur, request dan response, yang berbagi satu kerangka: start line, blok header, satu baris kosong, lalu body opsional. Baris kosong itu bukan hiasan: ia sinyal yang tidak ambigu bahwa header sudah selesai dan body (jika ada) dimulai. Parser bergantung penuh padanya.
Pesan request, baris demi baris
POST /api/v1/notes HTTP/1.1 # request line: method / target / version
Host: api.example.com # mandatory in HTTP/1.1 (virtual hosting)
User-Agent: curl/8.4.0
Authorization: Bearer eyJhbGc... # headers: case-insensitive key: value
Content-Type: application/json # what the body IS
Content-Length: 33 # body size in bytes, tells server where it ends
# blank line: headers end, body begins
{"title":"buy milk","done":false} # request body
Request line punya tiga bagian: method (maksud dari request, bagian 6), request target (biasanya path plus query string opsional, seperti /notes?done=false), dan versi protokol. Header Host terpisah dari target karena satu server bisa menampung banyak domain. Content-Length lebih penting dari kelihatannya: pada koneksi persisten tempat banyak pesan mengalir berurutan, ia memberi tahu penerima berapa byte body yang harus dibaca sebelum pesan berikutnya mulai. Salah hitung dan seluruh koneksi jadi tidak sinkron.
Pesan respons
HTTP/1.1 201 Created # status line: version / code / reason phrase
Content-Type: application/json
Content-Length: 41
Date: Sat, 30 May 2026 12:00:00 GMT
# blank line
{"id":42,"title":"buy milk","done":false}
Status line membawa versi, status code numerik yang jadi acuan percabangan klien (bagian 10), dan reason phrase yang bisa dibaca manusia ("Created", "Not Found"). Reason phrase murni informasi: klien harus bertindak berdasarkan angka, jangan pernah teksnya, karena teksnya bervariasi dan dihilangkan total di HTTP/2.
Tipe MIME: kosakata Content-Type
Sejak HTTP/1.0, protokol yang sama membawa konten apa pun: JSON, HTML, gambar, video, blob biner. Penerima perlu tahu yang mana, dan itu tugas tipe MIME (media type) di header Content-Type. Tatabahasanya type/subtype dengan parameter opsional: application/json; charset=utf-8 atau multipart/form-data; boundary=----X7. Parameter charset memberi tahu parser encoding teksnya; menghilangkannya pada teks bisa memunculkan mojibake. Salah mengisi Content-Type adalah bug umum: JSON yang dilabeli text/plain tidak diparse banyak klien; server juga bisa menolak body yang tipe deklarasinya tidak ia terima dengan 415 Unsupported Media Type.
| Tipe MIME | Dipakai untuk |
|---|---|
| application/json | Bawaan API modern. |
| application/x-www-form-urlencoded | Posting form HTML klasik, di-encode sebagai key=val&k2=v2. |
| multipart/form-data | Upload file dan kolom biner campuran (bagian 17). |
| text/html, text/plain | Halaman web dan teks polos. |
| application/octet-stream | "Sekadar byte" apa pun; biasanya memicu unduhan file. |
| text/event-stream | Streaming Server-Sent Events (bagian 17). |
Header dikirim ke kabel sebelum status line dan body; penerima membacanya lebih dulu untuk tahu cara menafsirkan yang menyusul. Jadi begitu Anda menulis status code (di Go, w.WriteHeader()) atau mulai streaming body, header apa pun yang Anda set setelahnya dibuang diam-diam: byte-nya sudah lewat. Aturan tetap di semua framework: set header -> tulis status -> tulis body. Melupakan urutan ini adalah penyebab bug klasik "header saya tidak terkirim".
package main
import (
"encoding/json"
"io"
"net/http"
)
type Note struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
func createNote(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body) // 1. read request body
defer r.Body.Close()
var in Note
if err := json.Unmarshal(body, &in); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json") // 2. headers FIRST
w.WriteHeader(http.StatusCreated) // 3. status
in.ID = 42
json.NewEncoder(w).Encode(in) // 4. body LAST
}
func main() {
http.HandleFunc("POST /api/v1/notes", createNote) // Go 1.22 method routing
http.ListenAndServe(":8080", nil)
}
from fastapi import FastAPI, Response, status
from pydantic import BaseModel
app = FastAPI()
class Note(BaseModel):
title: str
done: bool = False
@app.post("/api/v1/notes", status_code=status.HTTP_201_CREATED)
def create_note(note: Note, response: Response):
# FastAPI parses + validates the JSON body; a malformed body -> 422 automatically.
response.headers["Content-Type"] = "application/json"
return {"id": 42, **note.model_dump()}
# run: uvicorn main:app --port 8080
Kenapa Header Ada
Header adalah pasangan key-value berisi metadata yang menempel pada request atau response, berada di antara start line dan body. Pertanyaan yang wajar: kenapa harus bagian terpisah? Kenapa tidak taruh informasi ini di URL, atau di dalam body?
Memisahkan metadata dari payload memberi dua sifat fondasi:
- Ekstensibel: kemampuan baru ditambahkan cukup dengan mendefinisikan header baru, tanpa mengubah struktur protokol sama sekali. Kebijakan keamanan, client hints, negosiasi kompresi, header
X-khusus aplikasi Anda, ID tracing, semua ini menempel ke HTTP selama bertahun-tahun tanpa merusak klien lama, karena apa pun yang tidak mengenali sebuah header cukup mengabaikannya. Protokol tumbuh tanpa riak versi. - Kendali jarak jauh: header membuat klien bisa mengirim instruksi dan preferensi yang mengarahkan perilaku server, dan server bisa mengirim arahan balik. Klien bilang "saya prefer JSON" (
Accept), "cache ini sejam" adalah jawaban server (Cache-Control), "ini identitas saya" (Authorization), "saya bicara ke domain ini" (Host). Body request yang sama bisa menghasilkan respons berbeda hanya karena header-nya, ini tombol dan saklar dari percakapan.
Nama header juga tidak sensitif huruf dan urutannya tidak penting, sebab itu framework menormalkannya. Dan karena perantara ikut membaca header, di sinilah caching, negosiasi konten, autentikasi, dan kebijakan keamanan diekspresikan. Bab-bab selanjutnya, sebagian besar, adalah tur keliling header tertentu dan mesin di belakangnya.
Jenis-jenis Header HTTP
Ratusan header yang terdefinisi jatuh ke beberapa ember fungsional. Mengetahui embernya memberi tahu siapa yang set header dan apa yang diaturnya.
| Kategori | Tujuan | Contoh |
|---|---|---|
| Request | Dikirim klien; menggambarkan klien, kemampuannya, dan yang ia inginkan | User-Agent, Authorization, Accept, Host, Cookie |
| General | Berlaku untuk request dan response; metadata tentang pesan itu sendiri | Date, Connection, Cache-Control |
| Representation | Menggambarkan body agar lawan bicara bisa menafsirkan dan memprosesnya | Content-Type, Content-Length, Content-Encoding, ETag |
| Security | Dikirim server untuk mengendalikan perilaku klien (browser) dan memblokir kelas serangan | Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, Set-Cookie |
Header keamanan: serangan yang dihentikan masing-masing
Header keamanan pantas dipahami satu per satu, karena masing-masing adalah penangkal langsung untuk serangan spesifik yang punya nama. Mereka bekerja dengan menyuruh browser menolak perilaku berbahaya:
Strict-Transport-Security(HSTS) memberi tahu browser: "selama N detik ke depan, hubungi aku hanya lewat HTTPS, meski tautannya bilang http://". Ini mengalahkan serangan penurunan protokol, tempat penyerang di jaringan menurunkan koneksi ke HTTP polos untuk membaca atau mengubahnya.Content-Security-Policy(CSP), daftar izin tempat skrip, gaya, gambar, dan resource lain boleh dimuat (mis.default-src 'self'). Tugas utamanya menetralkan cross-site scripting (XSS): meski penyerang menyuntikkan tag<script>, browser menolak menjalankannya kecuali sumbernya ada di daftar izin.X-Frame-Options: DENYmelarang halaman Anda disematkan di<iframe>situs lain. Ini memblokir clickjacking, tempat penyerang menimpa halaman asli (tak terlihat) Anda di atas umpan, mengelabui pengguna mengklik tombol yang tidak mereka lihat.X-Content-Type-Options: nosniffmenghentikan browser menebak ("sniffing") tipe sebuah resource saatContent-Typeyang dideklarasikan terlihat salah. Sniffing bisa dikelabui untuk menjalankan gambar yang di-upload sebagai skrip, itulah serangan MIME-sniffing klasik, dan header ini mematikannya.Set-CookiedenganHttpOnlydanSecure.HttpOnlymenyembunyikan cookie dari JavaScript (jadi XSS yang berhasil tidak bisa mencuri sesi), danSecurememastikan cookie hanya dikirim lewat HTTPS (jadi tidak bocor lewat teks polos).
Karena header ini seharusnya berlaku untuk setiap respons, set sekali di middleware, bukan per handler, satu titik pemotongan yang membungkus seluruh aplikasi:
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
h.Set("Content-Security-Policy", "default-src 'self'")
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r) // headers MUST be set before this call (see sec 3)
})
}
// http.ListenAndServe(":8080", securityHeaders(mux))
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeaders(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
resp = await call_next(request)
resp.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
resp.headers["Content-Security-Policy"] = "default-src 'self'"
resp.headers["X-Frame-Options"] = "DENY"
resp.headers["X-Content-Type-Options"] = "nosniff"
return resp
app.add_middleware(SecurityHeaders)
Method HTTP: Bedah Mendalam
Method (alias verb) mengekspresikan maksud semantik dari sebuah request; ia memberi tahu server aksi macam apa yang Anda mau, terlepas dari URL-nya. RFC 9110 mendefinisikan sembilan. Masing-masing membawa tiga sifat independen, dan memahaminya dengan benar adalah pembeda antara API yang benar, aman diulang, dan ramah cache dengan API yang penuh bug.
Tiga sifatnya, secara presisi
- Safe: request hanya membaca; menjalankannya tidak boleh mengubah state server yang teramati. Maksudnya: agen otomatis, crawler pencarian, prefetcher tautan, pemindai URL antivirus, bebas menembakkan request safe sesuka hati. Jika GET menghapus sesuatu, crawler akan menghancurkan data Anda. (Mencatat log request atau menaikkan counter view masih ditoleransi; resource tempat klien bertindak tidak boleh berubah.)
- Idempotent: efek dari N request identik sama dengan efek satu request. Inilah sifat yang membuat request aman diulang setelah kegagalan jaringan: jika klien Anda timeout dan tidak yakin request-nya sampai, ia bisa mengirim ulang tanpa takut efeknya jadi ganda. Yang penting, idempotensi soal state server, bukan body respons: dua DELETE boleh mengembalikan 200 lalu 404, tapi state akhirnya ("resource-nya hilang") sama, jadi DELETE idempotent.
- Cacheable: respons boleh disimpan cache (browser, CDN, proxy) dan dipakai ulang untuk menjawab request setara yang datang belakangan tanpa menghubungi origin server, menghemat round trip dan beban.
Setiap method safe otomatis idempotent: jika request tidak mengubah apa pun, melakukannya dua kali juga tidak mengubah apa pun. Kebalikannya tidak berlaku: PUT dan DELETE idempotent tapi tidak safe, karena keduanya memang mengubah state, hanya saja secara deterministik, ke hasil akhir yang sama berapa pun kali diulang.
Tabel sifat yang otoritatif (RFC 9110)
| Method | Safe | Idempotent | Cacheable | Body request | Body respons |
|---|---|---|---|---|---|
| GET | Ya | Ya | Ya | Tidak | Ya |
| HEAD | Ya | Ya | Ya | Tidak | Tidak |
| OPTIONS | Ya | Ya | Tidak | Tidak | Ya |
| TRACE | Ya | Ya | Tidak | Tidak | Ya |
| PUT | Tidak | Ya | Tidak | Ya | Opsional |
| DELETE | Tidak | Ya | Tidak | Opsional | Opsional |
| POST | Tidak | Tidak | Bersyarat* | Ya | Ya |
| PATCH | Tidak | Tidak | Bersyarat* | Ya | Opsional |
| CONNECT | Tidak | Tidak | Tidak | Tidak | Ya |
*POST dan PATCH hanya cacheable jika respons secara eksplisit memuat info kesegaran dan Content-Location yang cocok, jarang terjadi dalam praktik, jadi perlakukan keduanya sebagai non-cacheable.
"Beri aku resource ini." Kuda beban jalur baca.
GET seharusnya hanya mengambil data dan tidak membawa makna semantik apa pun di body; kebanyakan server, proxy, dan cache mengabaikan atau menolak body GET, jadi semua input harus lewat URL. Filter, sorting, dan pagination karena itu hidup di query string (/notes?done=false&sort=created&limit=20), jangan pernah di body. Karena GET safe dan cacheable, responsnya bisa disimpan di setiap lapisan, cache browser, CDN, proxy korporat, itulah mesin performa web. Ini juga persis alasan perubahan state tidak boleh bersembunyi di balik GET: prefetcher, crawler, atau pemanasan cache akan menembakkannya dan memicu perubahan tanpa ada pengguna yang mengklik.
Perilaku di backend
- Ketemu -> 200 OK dengan resource di body.
- Tidak ada -> 404 Not Found.
- Salinan cache klien masih valid -> 304 Not Modified, body kosong (bagian 12).
"Beri aku metadata yang akan kamu kirim untuk GET, tapi tanpa payload."
Server mengembalikan status dan header yang sama persis seperti GET, termasuk Content-Length, Content-Type, ETag, dan Last-Modified, tapi body-nya dihilangkan total. Berguna saat Anda butuh metadata tanpa membayar biaya unduhan: memeriksa apakah resource ada (200 vs 404), membaca ukuran file sebelum memutuskan mengambilnya, memvalidasi kesegaran salinan cache, atau menjalankan health check murah ke sebuah URL. Kebanyakan framework mengimplementasikan HEAD otomatis dengan meneruskannya ke handler GET lalu membuang body yang dihasilkan, sebab itu jarang ada yang menulis handler HEAD terpisah.
"Ini datanya, lakukan sesuatu dengannya yang mengubah state."
POST adalah verb pengubah state serba guna: membuat record, mengirim form, memulai background job, memproses pembayaran, apa pun yang tidak cocok dengan verb yang lebih spesifik. Ciri utamanya (dan yang berbahaya) adalah ia tidak idempotent: dua POST identik membuat dua resource. Ini mekanisme di balik order ganda saat pengguna mengklik dua kali tombol "beli", dan di balik tagihan ganda saat jaringan yang tidak stabil membuat klien mengulang pembayaran yang tidak yakin sudah sampai. Solusinya adalah idempotency key (bagian 7), intinya cara memasang idempotensi ke method yang tidak memilikinya.
Perilaku di backend
- Resource dibuat -> 201 Created, idealnya dengan header
Locationyang menunjuk ke URL baru supaya klien tahu di mana ia sekarang berada. - Diterima untuk pemrosesan asinkron (belum selesai) -> 202 Accepted.
- Validasi gagal -> 400 (request salah bentuk) atau 422 (bentuknya benar tapi tidak valid secara semantik).
- Bentrok aturan bisnis, mis. username duplikat -> 409 Conflict.
"Buat resource di URL ini persis seperti body ini."
PUT melakukan penggantian total: klien memasok representasi baru yang lengkap dari resource dan menyasar URL spesifik yang sudah diketahui (biasanya yang dipilih klien sendiri, seperti /users/alice/avatar). Inilah yang membuatnya idempotent: mengirim body lengkap yang sama sepuluh kali membuat resource dalam state akhir yang identik tiap kali, karena tiap request menimpa, bukan menumpuk. PUT juga bisa membuat resource yang belum ada (lalu kembalikan 201 Created) atau mengganti yang sudah ada (lalu 200 OK atau 204 No Content).
Pakai POST ketika server yang memutuskan identitas resource baru: POST /notes dan server menjawab Location: /notes/42. Pakai PUT ketika klien sudah tahu URL target lengkapnya: PUT /users/alice/settings. Konsekuensi praktisnya: PUT-untuk-membuat idempotent (aman diulang), POST-untuk-membuat tidak (pengulangan bisa menggandakan). Memilih yang tepat berarti memilih apakah pengulangan aman.
"Ubah hanya field ini; biarkan yang lain apa adanya."
Kalau PUT mengganti seluruh resource, PATCH hanya mengirim seperangkat perubahan: mis. {"done": true} untuk membalik satu field tanpa mengirim ulang seluruh objek. Ini lebih efisien dan menghindari terhapusnya field yang tidak Anda maksud. PATCH tidak dijamin idempotent oleh spesifikasi (tanda * di tabel): patch seperti "tambahkan item ini ke daftar" atau "naikkan counter 1" menghasilkan hasil berbeda tiap kali dijalankan. Patch yang menetapkan field ke nilai tetap biasanya idempotent dalam praktik, tapi tidak bisa diandalkan secara universal. Server mengiklankan format dokumen patch yang ia terima, seperti JSON Merge Patch atau JSON Patch yang lebih presisi, lewat header respons Accept-Patch.
PATCH untuk pembaruan parsial, PUT untuk penggantian total. Banyak developer memakai PUT padahal maksudnya PATCH (lalu harus mengirim semua field supaya tidak ter-null-kan). Beralih ke PATCH sebagai bawaan kecuali Anda benar-benar berniat menimpa seluruh resource.
"Hapus resource ini."
DELETE is idempotent for a subtle but important reason: deleting once removes the resource, and deleting again changes nothing because it's already gone. The responses may differ across attempts (the first returns 200/204, a repeat returns 404), but the state of the server, "this resource does not exist", is identical after one call or ten. That state-equivalence is exactly what idempotency means, which is why a client can safely retry a DELETE that timed out. Return 204 No Content when there's nothing to send back, or 200 with a body describing what was removed.
"Apa saja yang bisa kulakukan pada resource ini?"
OPTIONS menanyakan method dan header apa yang didukung sebuah resource tanpa mengeksekusinya; server menjawab dengan header Allow (mis. Allow: GET, POST, OPTIONS). Peran utamanya di dunia nyata adalah CORS preflight (bagian 9): sebelum browser mengirim request lintas-origin yang "tidak sederhana", ia otomatis menembakkan OPTIONS untuk meminta izin, dan baru lanjut jika respons server menyetujui method, header, dan origin-nya. Anda hampir tidak pernah memanggil OPTIONS manual, tapi akan sering melihatnya di tab Network browser bersanding dengan request asli Anda.
"Buka tunnel dua arah mentah ke host ini lewat kamu, proxy."
CONNECT hampir eksklusif dipakai forward proxy untuk menangani HTTPS. Saat browser yang dikonfigurasi memakai proxy ingin mengakses situs HTTPS, ia mengirim CONNECT example.com:443 ke proxy, yang membuka tunnel TCP ke tujuan lalu meneruskan byte terenkripsi ke dua arah secara buta. Karena lalu lintas di dalamnya TLS end-to-end, proxy tidak bisa membaca atau mengubahnya, ia hanya menyekop byte. Anda praktis tidak akan pernah mengimplementasikan CONNECT di server aplikasi; ia hidup di perangkat lunak proxy dan infrastruktur (bagian 18).
"Gema balik request-ku supaya bisa kulihat apa yang diubah perantara."
TRACE menyuruh server menggema balik request yang ia terima, jadi klien bisa melihat persis bagaimana proxy dan gateway di sepanjang jalur memodifikasi pesan, alat debug untuk rantai request. Dalam praktik ia dinonaktifkan hampir di mana-mana, karena memungkinkan kelas serangan bernama Cross-Site Tracing (XST) yang bisa membocorkan header yang tadinya tersembunyi (seperti cookie) ke skrip jahat. Kebanyakan server dan framework menolak TRACE mentah-mentah. Ketahui keberadaannya demi kelengkapan; Anda tidak akan memakainya.
Merangkai method ke handler
mux := http.NewServeMux()
// Go 1.22+ binds method + path. An unmatched method auto-returns 405 Method Not Allowed.
mux.HandleFunc("GET /notes", listNotes) // safe, cacheable read
mux.HandleFunc("GET /notes/{id}", getNote)
mux.HandleFunc("HEAD /notes/{id}", getNote) // reuse GET; framework drops the body
mux.HandleFunc("POST /notes", createNote) // create (server assigns id)
mux.HandleFunc("PUT /notes/{id}", putNote) // full replace (idempotent)
mux.HandleFunc("PATCH /notes/{id}", patchNote) // partial update
mux.HandleFunc("DELETE /notes/{id}", deleteNote) // remove (idempotent)
func getNote(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // built-in path params
note, err := db.Find(id)
if err == ErrNotFound { http.NotFound(w, r); return } // 404
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(note) // 200
}
from fastapi import FastAPI, HTTPException
app = FastAPI() # auto-returns 405 for unregistered methods, 422 on bad bodies
@app.get("/notes") # safe, cacheable read (list)
def list_notes(done: bool | None = None, limit: int = 20): ...
@app.get("/notes/{id}") # GET also serves HEAD automatically
def get_note(id: int):
note = db.find(id)
if note is None:
raise HTTPException(404, "not found") # 404
return note # 200
@app.post("/notes", status_code=201) # create (server assigns id)
def create_note(note: Note): ...
@app.put("/notes/{id}") # full replace (idempotent)
def put_note(id: int, note: Note): ...
@app.patch("/notes/{id}") # partial update
def patch_note(id: int, patch: dict): ...
@app.delete("/notes/{id}", status_code=204) # remove (idempotent)
def delete_note(id: int): ...
Idempotensi dalam Praktik
Idempotensi adalah sifat yang membuat request aman diulang, dan pengulangan bukan pilihan dalam sistem terdistribusi, melainkan keniscayaan. Jaringan menjatuhkan paket, klien timeout tanpa tahu apakah request sudah sampai ke server, pengguna yang tidak sabar mengklik dua kali, aplikasi mobile mengirim ulang saat radio tersambung lagi. GET, PUT, dan DELETE idempotent sejak desain, jadi mengulangnya tidak berbahaya. POST tidak, dan itu akar dari bug record ganda dan tagihan ganda.
Masalah yang lebih dalam adalah "hasil tidak diketahui": saat klien mengirim POST dan koneksi putus sebelum respons tiba, klien benar-benar tidak bisa membedakan apakah server memprosesnya. Mengulang berisiko menggandakan; tidak mengulang berisiko kehilangan operasi. Idempotency key menyelesaikan dilema ini dengan bersih.
Klien membuat key unik (biasanya UUID) untuk operasi logis dan mengirimnya sebagai header Idempotency-Key. Server menyimpan key tersebut bersama hasil eksekusi sukses pertama. Jika key yang sama datang lagi, karena klien mengulang, server mengenalinya dan mengembalikan hasil yang tersimpan tanpa menjalankan ulang pekerjaannya. Operasi pun tereksekusi paling banyak sekali berapa pun jumlah pengiriman. (MDN sekarang mendokumentasikan Idempotency-Key sebagai header yang distandarkan, dan API pembayaran seperti Stripe memopulerkan pola ini.)
POST /payments with Idempotency-Key: 9f8c-....key -> result,
return 201.Pertimbangan implementasi
- Simpan key secara permanen: map di memori pada contoh kode di bawah hanya ilustrasi; untuk produksi pakai Redis atau tabel database supaya key bertahan dari restart dan dipakai bersama semua instance server (ingat, node mana pun bisa menerima pengulangan, bagian 1).
- Beri key TTL: buang key tersimpan setelah, misalnya, 24 jam supaya penyimpanan tidak tumbuh tanpa batas.
- Simpan sebelum merespons: catat hasilnya sebelum mengirim respons, supaya crash di antara "pekerjaan selesai" dan "key tersimpan" tidak menghilangkan jaminan dedup.
var seen sync.Map // key -> []byte result. Use Redis with a TTL in production.
func createPayment(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "missing Idempotency-Key", http.StatusBadRequest)
return
}
if cached, ok := seen.Load(key); ok { // replay: return the stored result
w.WriteHeader(http.StatusOK)
w.Write(cached.([]byte))
return
}
result := charge(r) // the real, non-idempotent work
seen.Store(key, result) // remember it BEFORE responding
w.WriteHeader(http.StatusCreated)
w.Write(result)
}
from fastapi import Header, HTTPException
seen: dict[str, dict] = {} # use Redis with a TTL in production
@app.post("/payments", status_code=201)
def create_payment(payload: dict, idempotency_key: str = Header(...)):
if idempotency_key in seen: # replay: return the stored result
return seen[idempotency_key]
result = charge(payload) # the real, non-idempotent work
seen[idempotency_key] = result # remember it BEFORE responding
return result
Idempotency key melindungi dari eksekusi ganda. Bahaya yang berbeda tapi berkerabat, dua klien saling menimpa edit ("lost update"), butuh alat pelengkap: konkurensi optimistis dengan If-Match dan ETag, dibahas di bagian 13.
Cookie, Sesi & Autentikasi
HTTP stateless (bagian 1), tapi setiap aplikasi sungguhan perlu tahu "request ini dari pengguna yang sama dengan yang tadi login". Protokolnya tidak menyediakan memori, jadi kita membangun ulang identitas di tiap request dengan menyuruh klien mengirim ulang kredensial kecil. Ada dua arsitektur dominan untuk ini, tapi keduanya berangkat dari primitif yang sama: cookie.
Cara kerja cookie secara mekanis
Seluruh mekanisme cookie cuma dua header. Server menempelkan header Set-Cookie pada sebuah respons; browser menyimpan nilainya lalu otomatis memasukkannya ke header Cookie pada setiap request berikutnya ke domain itu, tanpa kode di halaman yang memintanya. Otomatisitas itu sekaligus kenyamanan (sesi "tinggal jalan") dan bahayanya (inilah yang membuat CSRF mungkin terjadi, lihat SameSite di bawah).
# response from the server, sets the cookie once
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/
# every later request from the browser, sent automatically
Cookie: session=abc123
Atributnya, dan kenapa masing-masing penting bagi keamanan
HttpOnly, cookie tak terlihat oleh JavaScript (document.cookietidak bisa membacanya). Artinya, bahkan injeksi XSS yang berhasil tidak bisa membocorkan token sesi.Secure, cookie hanya dikirim lewat HTTPS, jadi tidak bisa bocor lewat koneksi teks polos yang sedang disadap penyerang.SameSitemengendalikan apakah cookie ikut pada request lintas-situs, pertahanan inti terhadap CSRF (cross-site request forgery).Stricttidak pernah mengirimnya lintas-situs;Lax(bawaan modern) mengirimnya pada navigasi tingkat atas tapi tidak pada sub-request lintas-situs;Noneselalu mengirimnya tapi lalu wajibSecure.Max-Age/Expires, masa hidup; tanpa keduanya cookie menjadi "session cookie" yang mati saat browser ditutup.Domain/Path, membatasi host dan jalur URL mana yang menerimanya.
Dua arsitektur autentikasi
Pilihan di antara keduanya intinya "state sesi tinggal di mana?", dan pilihan itu berimbas ke skalabilitas serta pencabutan.
| Session cookie (ber-state) | Token / JWT (stateless) | |
|---|---|---|
| Tempat state | Server menyimpan sesi lengkap; cookie hanya memegang ID buram | Klien memegang token bertanda tangan berisi klaim; server tidak menyimpan apa pun |
| Dikirim via | Header Cookie (otomatis) |
Authorization: Bearer <token> (di-set eksplisit oleh klien) |
| Verifikasi request | Cari ID sesi di penyimpanan tiap request | Verifikasi tanda tangan kriptografis secara lokal, tanpa pencarian |
| Skalabilitas horizontal | Butuh session store bersama (mis. Redis) yang bisa dijangkau semua node | Sepele, node mana pun bisa verifikasi tanda tangan tanpa state bersama |
| Pencabutan | Mudah, tinggal hapus sesi di sisi server | Sulit, token valid sampai kedaluwarsa; butuh denylist atau umur pendek + refresh token |
Sebuah JWT adalah tiga bagian base64url yang disambung titik: header.payload.signature. Header menyebut algoritmanya, payload memegang klaim (user ID, kedaluwarsa), dan signature dihitung dari dua bagian pertama dengan secret atau private key. Siapa pun bisa membaca JWT (ini tidak dienkripsi, hanya di-encode), tapi hanya pemegang key yang bisa memalsukan signature yang valid, dan itulah yang membuat server percaya tanpa pencarian database. Penyempurnaan yang umum: access token berumur pendek plus refresh token berumur panjang, untuk meringankan kelemahan pencabutan.
Bagaimana JWT mencegah pemalsuan (di-encode vs dienkripsi)
Titik kebingungan yang umum: header dan payload JWT hanya di-encode base64url, tidak dienkripsi. Siapa pun yang mencegat token bisa meng-decode-nya dan membaca klaim dalam teks polos, termasuk field seperti "role": "user". Lalu kenapa penyerang tidak bisa meng-decode, mengubah role menjadi "admin", dan mengirim balik token yang sudah diedit?
Jawabannya di segmen ketiga: signature. Ia dihitung sebagai HMAC(header + payload, secret) (atau padanan private key) memakai secret key yang tidak pernah keluar dari server. Saat token yang dipalsukan datang, server menghitung ulang signature atas payload (yang kini berbeda) dengan secret yang sama; karena payload berubah, hash yang dihasilkan benar-benar berbeda dari yang menempel di token. Perbandingannya gagal dan server menolak request dengan 401, sebelum klaim yang dimodifikasi sempat dipercaya.
Jangan pernah menaruh rahasia di payload JWT, siapa pun bisa membacanya. Signature hanya menjamin payload tidak diubah setelah diterbitkan, bukan bahwa isinya rahasia. Tanpa secret key server, memalsukan signature yang valid untuk payload yang dimodifikasi secara komputasi mustahil.
Di sisi protokol, header respons WWW-Authenticate (dipasangkan dengan 401) memberi tahu klien skema autentikasi mana yang dipakai, dan Authorization adalah header request yang membawa kredensial kembali.
// Set a secure session cookie after login (stateful)
func login(w http.ResponseWriter, r *http.Request) {
sid := newSessionID()
sessions.Store(sid, userID) // server-side session store (use Redis)
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sid,
HttpOnly: true, // JS can't read it
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode, // CSRF defense
MaxAge: 3600,
Path: "/",
})
}
// Bearer-token auth middleware (stateless)
func requireToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") || !validJWT(auth[7:]) {
w.Header().Set("WWW-Authenticate", `Bearer`)
http.Error(w, "unauthorized", http.StatusUnauthorized) // 401
return
}
next.ServeHTTP(w, r)
})
}
from fastapi import Response, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
# Set a secure session cookie after login (stateful)
@app.post("/login")
def login(response: Response):
sid = new_session_id()
sessions[sid] = user_id # server-side session store (use Redis)
response.set_cookie(
key="session", value=sid,
httponly=True, # JS can't read it
secure=True, # HTTPS only
samesite="strict", # CSRF defense
max_age=3600, path="/",
)
# Bearer-token auth dependency (stateless)
bearer = HTTPBearer()
def require_token(cred: HTTPAuthorizationCredentials = Depends(bearer)):
if not valid_jwt(cred.credentials):
raise HTTPException(401, "unauthorized",
headers={"WWW-Authenticate": "Bearer"})
@app.get("/me")
def me(_=Depends(require_token)):
return {"ok": True}
CORS & Alur Preflight
Untuk memahami CORS, pahami dulu aturan yang ia longgarkan. Browser menegakkan Same-Origin Policy: JavaScript di halaman dari satu origin tidak boleh membaca respons dari origin yang berbeda. "Origin" adalah tiga serangkai skema + host + port: https://example.com dan https://example.com:8443 dan http://example.com semuanya origin yang berbeda. Kebijakan ini ada karena browser otomatis menempelkan cookie Anda ke request (bagian 8); tanpanya, situs jahat yang Anda kunjungi bisa diam-diam menembakkan request terautentikasi ke API bank Anda dan membaca responsnya. CORS (Cross-Origin Resource Sharing) adalah mekanisme terkendali yang dipakai server untuk ikut memilih origin mana yang boleh membaca responsnya.
CORS ditegakkan sepenuhnya oleh browser, bukan server. Server hanya mengiklankan siapa yang ia izinkan, lewat header respons; browser yang memeriksa header itu dan memutuskan apakah respons diserahkan ke JavaScript halaman atau diblokir. Inilah kenapa error CORS muncul di console browser tapi log server menampilkan 200 yang wajar: request-nya berhasil, browser-nya saja yang menolak membagikan hasilnya. Ini juga kenapa curl, aplikasi mobile, dan panggilan server-ke-server mengabaikan CORS sepenuhnya: tidak ada browser yang menegakkannya.
Alur 1: request sederhana
Untuk request yang dianggap "sederhana" oleh spesifikasi, GET, POST, atau HEAD yang hanya memakai header standar dan tipe konten umum, browser tidak repot minta izin lebih dulu. Ia langsung mengirim request (dengan header Origin) dan memeriksa responsnya setelahnya.
Origin: https://example.com and sends the request.
Access-Control-Allow-Origin echoing that origin (or *) in the response.Alur 2: request yang lewat preflight
Untuk yang lebih berisiko, browser mengirim request pendahuluan "bolehkah?" lebih dulu. Preflight terpicu jika salah satu kondisi ini berlaku:
- Method-nya bukan GET / POST / HEAD (jadi PUT, PATCH, DELETE selalu preflight).
- Request membawa header non-sederhana, yang paling umum
Authorizationatau headerX-khusus apa pun. Content-Type-nya di luar himpunan sederhana, danapplication/jsonmemang di luarnya.
Karena request API JSON terautentikasi yang khas kena ketiganya (body JSON, header Authorization, sering juga method non-GET), hampir setiap panggilan API nyata lewat preflight. Itulah kenapa tab Network Anda menampilkan OPTIONS berpasangan tepat sebelum tiap panggilan asli: itu bukan bug, itu browser yang minta izin.
OPTIONS /resource with Access-Control-Request-Method
and -Request-Headers, asking "may I send this method with these headers from my origin?", no
body.Access-Control-Allow-Origin / -Methods / -Headers, plus -Max-Age telling the
browser how long it may cache this approval (e.g. 24h) so it can skip the preflight on future identical
calls.Satu kerutan tambahan untuk request berkredensial (yang membawa cookie): server juga harus mengirim Access-Control-Allow-Credentials: true, dan dalam mode itu ia tidak boleh memakai wildcard * untuk origin, harus menyebut origin persisnya. Ini mencegah server tanpa sengaja membocorkan data terautentikasi ke situs mana pun.
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Access-Control-Allow-Origin", "https://example.com") // exact, not * (credentials)
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE")
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
h.Set("Access-Control-Allow-Credentials", "true")
h.Set("Access-Control-Max-Age", "86400") // cache the approval for 24h
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent) // 204: answer the preflight and stop
return
}
next.ServeHTTP(w, r)
})
}
from fastapi.middleware.cors import CORSMiddleware
# Handles the OPTIONS preflight and all the headers for you.
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"], # exact origin when credentials are on
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
allow_credentials=True,
max_age=86400, # cache the approval for 24h
)
Kesalahpahaman umum: apakah CORS melindungi server?
Pertanyaan yang sering muncul: "kalau skrip bisa mengabaikan CORS dengan curl atau HTTP client khusus, bukankah CORS sia-sia untuk keamanan?" Premisnya benar, CORS mudah dilewati di luar browser, tapi kesimpulannya salah, karena CORS sejak awal tidak dirancang untuk melindungi server; ia melindungi pengguna.
Saat alat seperti curl atau skrip khusus mengabaikan CORS, ia bisa membaca apa pun yang dikembalikan server, tapi ia tidak punya cookie atau token sesi pengguna, jadi server menolaknya sebagai tidak terautentikasi (bagian 8). CORS justru menegakkan Same-Origin Policy di dalam browser yang sah: jika pengguna login ke banknya lalu mengunjungi situs jahat, JavaScript situs itu berjalan di dalam browser pengguna sendiri, yang otomatis menempelkan cookie sesi bank ke request apa pun yang ia tembakkan. CORS adalah mekanisme browser bertanya ke bank "apakah origin jahat ini boleh membaca respons ini?", dan memblokir respons saat jawabannya tidak.
Server melindungi dirinya sendiri lewat autentikasi, otorisasi, dan token CSRF. CORS melindungi sesi pengguna yang sudah dipercaya dan membawa cookie dari dibaca oleh origin yang tidak pernah ia setujui.
Ancaman ekstensi jahat
Ekstensi browser yang jahat memutus seluruh rantai kepercayaan: ia berjalan di dalam browser yang dipercaya, berdampingan dengan halaman, jadi ia bisa melewati CORS, membaca cookie langsung, merekam ketukan tombol, dan menulis ulang request keluar sebelum sempat sampai ke jaringan. Mengamankan lingkungan klien sama pentingnya dengan mengamankan backend, tidak ada header sisi server yang menghentikan kode yang sudah berjalan di tempat cookie Anda tinggal.
Status Code
Status code tiga digit adalah vonis terstandar yang bisa dibaca mesin atas sebuah request. Nilainya: klien bisa memutuskan apa yang harus dilakukan, ulangi, redirect, autentikasi ulang, tampilkan error, cukup dengan membaca satu angka, tanpa mem-parse body atau menebak dari bentuknya. Sebelum status code ada, klien harus mengintip isi respons untuk menyimpulkan sukses atau gagal, rapuh dan tidak konsisten. Digit pertama mendefinisikan kelasnya, dan itu saja sudah memberi tahu Anda salah siapa sebuah masalah.
| Kelas | Makna | Yang bertanggung jawab |
|---|---|---|
| 1xx | Informatif, diterima, lanjutkan | - |
| 2xx | Sukses | - |
| 3xx | Redireksi, aksi lain dibutuhkan (bagian 11) | - |
| 4xx | Error klien, request-nya salah | Pemanggil |
| 5xx | Error server, server gagal memenuhi request yang valid | Server |
Kode yang benar-benar akan Anda pakai, dan kapan persisnya
| Kode | Nama | Kapan mengirimnya |
|---|---|---|
| 100 | Continue | Klien boleh mengirim body; dipakai dengan Expect: 100-continue supaya upload besar tidak terkirim sampai server memberi sinyal bersedia. |
| 101 | Switching Protocols | Upgrade diterima, mis. ke WebSocket (bagian 19). |
| 200 | OK | Sukses generik; resource dikembalikan. |
| 201 | Created | POST/PUT membuat resource; sertakan header Location yang menunjuk ke resource itu. |
| 202 | Accepted | Diterima untuk pemrosesan asinkron; pekerjaannya belum selesai. |
| 204 | No Content | Sukses tanpa ada yang dikembalikan (DELETE, OPTIONS preflight). |
| 206 | Partial Content | Request range terpenuhi (bagian 15). |
| 301 | Moved Permanently | Resource punya URL baru selamanya (bagian 11). |
| 302 | Found | Redirect sementara (bagian 11). |
| 304 | Not Modified | Validasi cache, salinan klien masih bagus (bagian 12). |
| 307 | Temporary Redirect | Seperti 302 tapi method & body dipertahankan (bagian 11). |
| 308 | Permanent Redirect | Seperti 301 tapi method & body dipertahankan (bagian 11). |
| 400 | Bad Request | Sintaks salah bentuk yang tidak bisa diparse server (JSON rusak, encoding buruk). |
| 401 | Unauthorized | Kredensial hilang atau tidak valid -> klien harus autentikasi. |
| 403 | Forbidden | Sudah terautentikasi, tapi tidak diizinkan melakukan ini. |
| 404 | Not Found | Tidak ada resource seperti itu, atau URL-nya salah. |
| 405 | Method Not Allowed | Resource valid, method salah; balas dengan header Allow yang mencantumkan method yang valid. |
| 409 | Conflict | Request bentrok dengan state saat ini (nama duplikat, versi tidak cocok). |
| 412 | Precondition Failed | Prasyarat If-Match/If-Unmodified-Since tidak terpenuhi (bagian 13). |
| 413 | Content Too Large | Body request melebihi batas server. |
| 415 | Unsupported Media Type | Server tidak menerima Content-Type body. |
| 422 | Unprocessable Content | Sintaksnya benar tapi datanya gagal validasi bisnis. |
| 429 | Too Many Requests | Rate limit terlampaui; sertakan header Retry-After. |
| 500 | Internal Server Error | Exception tak tertangani yang membuat handler crash, "kami yang rusak" generik. |
| 501 | Not Implemented | Method/fitur-nya belum didukung. |
| 502 | Bad Gateway | Proxy menerima respons tidak valid dari server upstream. |
| 503 | Service Unavailable | Sementara mati/kelebihan beban/pemeliharaan; sertakan Retry-After. |
| 504 | Gateway Timeout | Proxy menunggu upstream dan menyerah. |
401 Unauthorized sebenarnya berarti belum terautentikasi: "saya tidak tahu siapa Anda; kirim kredensial." 403 Forbidden berarti terautentikasi tapi tidak berwenang: "saya tahu persis siapa Anda, dan tetap tidak boleh." Pengguna yang login memukul endpoint khusus-admin mendapat 403, bukan 401. Menamai yang tepat membuat klien bereaksi dengan benar: 401 -> tampilkan layar login; 403 -> tampilkan "akses ditolak".
400 untuk request yang bahkan tidak bisa diparse server, JSON rusak, encoding salah bentuk. 422 untuk request yang parse-nya sempurna tapi tidak valid secara semantik, objek JSON yang rapi dengan field email berisi nomor telepon. FastAPI mengembalikan 422 untuk kegagalan validasi otomatisnya; banyak API memakai 400 untuk keduanya, tapi perbedaannya membantu klien membedakan "perbaiki sintaks" dari "perbaiki datamu".
5xx berarti server gagal menghormati request yang valid, jadi 500 di log Anda adalah bug Anda, bukan klien. Tapi 502 / 503 / 504 istimewa: Anda jarang menerbitkannya dari kode aplikasi. Mereka dihasilkan oleh proxy atau load balancer di depan Anda (nginx, Envoy, LB cloud) saat tidak bisa menjangkau aplikasi Anda, mendapatinya mati, atau menunggu terlalu lama. Melihatnya menunjuk ke infrastruktur dan kesehatan upstream, bukan logika handler, jalan pintas triase yang penting.
func getUser(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "login required", http.StatusUnauthorized) // 401: who are you?
return
}
user, err := db.Find(r.PathValue("id"))
if err == ErrNotFound {
http.Error(w, "no such user", http.StatusNotFound) // 404
return
}
if !user.VisibleTo(r) {
http.Error(w, "forbidden", http.StatusForbidden) // 403: not allowed
return
}
json.NewEncoder(w).Encode(user) // 200
}
from fastapi import HTTPException, Header
@app.get("/users/{id}")
def get_user(id: int, authorization: str = Header(default="")):
if not authorization:
raise HTTPException(401, "login required") # 401: who are you?
user = db.find(id)
if user is None:
raise HTTPException(404, "no such user") # 404
if not user.visible_to(authorization):
raise HTTPException(403, "forbidden") # 403: not allowed
return user # 200
Redireksi
Redirect membuat satu resource bisa hidup di lebih dari satu URL: server merespons dengan status 3xx dan header Location yang menyebut tujuan, lalu klien (browser yang mengikuti otomatis, atau library HTTP Anda) mengirim ulang request ke URL baru itu. Redirect menggerakkan migrasi URL, penegakan canonical-URL, upgrade HTTP->HTTPS, dan pola Post/Redirect/Get. Detil yang sering menjegal backend: apakah method dan body asli selamat melewati redirect, karena secara historis sering tidak.
| Kode | Permanen? | Method dipertahankan? | Kasus pakai |
|---|---|---|---|
| 301 Moved Permanently | Ya | Tidak dijamin (klien bisa mengubah POST->GET) | Resource pindah selamanya, perbarui bookmark, transfer peringkat SEO. |
| 302 Found | Tidak | Tidak dijamin (klien bisa mengubah POST->GET) | Relokasi sementara; tetap pakai URL asli. |
| 303 See Other | Tidak | Memaksa GET | Post/Redirect/Get: setelah POST, redirect ke halaman hasil supaya refresh browser tidak mengirim ulang form. |
| 307 Temporary Redirect | Tidak | Ya: method & body dipertahankan | Redirect sementara yang harus mengirim ulang POST/PUT yang sama beserta body-nya utuh. |
| 308 Permanent Redirect | Ya | Ya: method & body dipertahankan | Pindah permanen yang harus mengawetkan POST/PUT. |
Klien lama, saat menghadapi 301 atau 302 pada POST, akan diam-diam mengirim ulang sebagai GET dan membuang body: secara teknis pelanggaran spesifikasi, tapi begitu meluas sampai jadi perilaku yang diekspektasikan. Ambiguisitas itu ("apakah POST-ku selamat lewat redirect ini?") justru alasan 307 dan 308 diciptakan: keduanya menjamin method dan body dipertahankan. Jadi kalau pengiriman form yang di-redirect tiba-tiba tiba tanpa data, Anda memakai 301/302 padahal butuh 308/307. Sebaliknya, 303 sengaja memaksa GET, persis yang Anda mau untuk Post/Redirect/Get, supaya pengguna bisa me-refresh halaman hasil dengan aman.
// Permanent move that must keep the method/body -> 308
func oldRoute(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/person/"+r.PathValue("id"), http.StatusPermanentRedirect) // 308
}
// Post/Redirect/Get -> 303 so a browser refresh won't re-POST the form
func submitForm(w http.ResponseWriter, r *http.Request) {
id := save(r)
http.Redirect(w, r, "/results/"+id, http.StatusSeeOther) // 303 (forces GET)
}
from fastapi.responses import RedirectResponse
# Permanent move that must keep the method/body -> 308
@app.api_route("/user/{id}", methods=["GET", "POST"])
def old_route(id: int):
return RedirectResponse(f"/person/{id}", status_code=308)
# Post/Redirect/Get -> 303 so a browser refresh won't re-POST the form
@app.post("/submit")
def submit_form():
rid = save()
return RedirectResponse(f"/results/{rid}", status_code=303) # forces GET
Caching HTTP
Caching berarti menyimpan salinan respons supaya request berikutnya bisa dipenuhi tanpa mengambil ulang dari origin, memangkas bandwidth, latensi, dan beban server sekaligus. Ini boleh dibilang tuas terbesar performa web. Caching HTTP bekerja pada dua lapisan berbeda, dan mencampurkannya adalah sumber kebingungan yang umum:
- Kesegaran (freshness): "Boleh aku pakai salinanku tanpa bertanya dulu?" Jika respons masih segar, klien menyajikannya dari cache tanpa panggilan jaringan sama sekali.
- Validasi: "Salinanku mungkin basi; apakah benar berubah?" Ini round trip murah yang menjawab "tidak, pakai punyamu" atau konten baru.
Cache juga hidup di beberapa tingkat: cache browser privat (hanya untuk satu pengguna), dan cache bersama seperti CDN serta reverse proxy (melayani banyak pengguna). Arahan Cache-Control di bawah menentukan tingkat mana yang boleh menyimpan sebuah respons.
Lapisan 1: Kesegaran via Cache-Control
Server menstempel respons dengan berapa lama ia tetap bisa dipakai, jadi klien bebas memakai ulang sampai saat itu:
max-age=3600, segar selama satu jam; pakai ulang tanpa jaringan sampai kedaluwarsa.publicvsprivate: bolehkah cache bersama (CDN/proxy) menyimpannya, atau hanya browser pengguna sendiri? Pakaiprivateuntuk respons personal atau terautentikasi supaya CDN tidak menyajikan data satu pengguna ke pengguna lain.no-cache, berlawanan dengan intuisi, artinya "boleh disimpan, tapi wajib revalidasi ke origin sebelum tiap pemakaian". Ini "selalu periksa", bukan "jangan pernah cache".no-store, arahan "jangan pernah cache di mana pun" yang sesungguhnya, untuk data sensitif.immutable, "ini tidak akan berubah selama masa kesegarannya, jadi jangan revalidasi sekalipun". Pas untuk aset statis bernama hash konten (app.4f9a2c.js), yang perubahannya toh menghasilkan nama file baru.
Lapisan 2: Validasi via ETag & Last-Modified
Saat kesegaran habis, klien tidak serta-merta mengunduh ulang, ia merevalidasi. Respons asli membawa ETag (sidik jari/hash konten) dan/atau timestamp Last-Modified. Klien mengirim keduanya kembali sebagai header kondisional, dan jika tidak ada yang berubah server membalas 304 mungil tanpa body, menghemat seluruh payload.
ETag: "abc" +
Cache-Control: max-age=10.If-None-Match: "abc" (and/or
If-Modified-Since).ETag punya dua rasa: ETag kuat ("abc") menjamin identitas byte-per-byte, sementara yang lemah (W/"abc") hanya menjanjikan kesetaraan semantik, berguna saat, misalnya, timestamp di respons berbeda tapi konten yang bermakna tidak.
func getResource(w http.ResponseWriter, r *http.Request) {
body := loadResource()
sum := sha256.Sum256(body)
etag := `"` + hex.EncodeToString(sum[:8]) + `"` // fingerprint of the content
if r.Header.Get("If-None-Match") == etag { // client already has this exact version
w.WriteHeader(http.StatusNotModified) // 304, no body, payload saved
return
}
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "max-age=10")
w.Write(body) // 200 + body
}
import hashlib
from fastapi import Request, Response
@app.get("/resource")
def get_resource(request: Request):
body = load_resource()
etag = '"' + hashlib.sha256(body).hexdigest()[:16] + '"' # fingerprint
if request.headers.get("if-none-match") == etag: # already cached
return Response(status_code=304) # 304, no body, payload saved
return Response(content=body, status_code=200,
headers={"ETag": etag, "Cache-Control": "max-age=10"})
Mengelola ETag manual rawan error: lupa menaikkan satu saat ada perubahan, klien menyajikan data basi selamanya. Untuk aset statis pilih immutable + nama file berhash konten; untuk data API, cache sisi klien seperti React Query memberi kendali lebih halus. Dan jangan lupa Vary: ia memberi tahu cache header request mana yang membuat respons berbeda (mis. Vary: Accept-Encoding). Hilangkan dan cache bersama bisa menyajikan body gzip ke klien yang tidak minta, atau memberikan respons Prancis ke penutur Inggris.
Request Kondisional & Penguncian Optimistis
Validator yang sama yang menggerakkan caching (bagian 12) memecahkan masalah backend kedua yang lebih dalam: lost update. Bayangkan dua editor sama-sama membuka dokumen v1. Alice menyimpan perubahannya; Bob, yang masih bekerja dari v1, menyimpan sesaat kemudian, dan simpanan Bob diam-diam menimpa punya Alice, tanpa error dan tanpa peringatan. Kerja Alice tinggal lenyap. Request kondisional mencegah ini lewat kontrol konkurensi optimistis: alih-alih mengunci resource selama seseorang mengedit (pesimistis), semua orang dibiarkan mengedit bebas dan bentrokan hanya terdeteksi saat menyimpan.
Header kondisional
Masing-masing berbunyi "lanjutkan hanya jika kondisi tentang versi resource saat ini terpenuhi". Penamaannya simetris: If-None-Match untuk baca (lanjutkan jika berubah -> aku butuh salinan baru), If-Match untuk tulis (lanjutkan jika tidak berubah -> aman menimpa).
| Header | Lanjutkan hanya jika... | Pemakaian khas |
|---|---|---|
| If-None-Match | ...ETag-nya tidak cocok (resource berubah) | Caching pada GET -> mengembalikan 304 saat tidak berubah |
| If-Match | ...ETag-nya cocok (tidak berubah sejak kubaca) | Tulis aman pada PUT/PATCH/DELETE -> 412 saat bentrok |
| If-Modified-Since | ...berubah setelah timestamp ini | Cadangan caching saat tidak ada ETag |
| If-Unmodified-Since | ...tidak berubah sejak timestamp ini | Tulis aman berbasis timestamp |
| If-Range | ...resource tidak berubah (kalau tidak, kirim semua) | Unduhan yang bisa dilanjutkan (bagian 15) |
Alur penguncian optimistis
/doc/7 -> receives the body plus ETag: "v1".PUT /doc/7 with If-Match: "v1", "only
save if it's still v1, i.e. nobody changed it under me."ETag: "v2".Kunci pesimistis menahan satu baris tetap terkunci selama pengguna menatap form edit, bisa berjam-jam, memblokir semua orang dan berisiko deadlock jika penggunanya pergi. Konkurensi optimistis tidak menahan apa pun: semua pembaca dibiarkan lanjut dan bentrokan hanya terdeteksi di detik penulisan, murah, dengan membandingkan tag versi. Harganya: sesekali simpanan gagal dan harus diulang, yang hampir selalu merupakan pertukaran yang lebih baik. Beginilah editor kolaboratif, wiki, dan REST API yang dirancang baik mencegah penimpaan senyap.
func updateDoc(w http.ResponseWriter, r *http.Request) {
doc := db.Get(r.PathValue("id"))
want := r.Header.Get("If-Match") // the ETag the client last saw
if want == "" {
http.Error(w, "If-Match required", http.StatusBadRequest)
return
}
if want != doc.ETag { // someone changed it first -> conflict
http.Error(w, "version conflict", http.StatusPreconditionFailed) // 412
return
}
doc.Apply(r.Body)
doc.ETag = newETag() // bump the version
db.Save(doc)
w.Header().Set("ETag", doc.ETag)
w.WriteHeader(http.StatusOK)
}
from fastapi import Request, Response, HTTPException, Header
@app.put("/doc/{id}")
def update_doc(id: int, request: Request, if_match: str = Header(default="")):
doc = db.get(id)
if not if_match:
raise HTTPException(400, "If-Match required")
if if_match != doc.etag: # someone changed it first -> conflict
raise HTTPException(412, "version conflict") # 412
doc.apply(request)
doc.etag = new_etag() # bump the version
db.save(doc)
return Response(status_code=200, headers={"ETag": doc.etag})
Negosiasi Konten & Kompresi
Satu URL bisa punya banyak representasi: artikel yang sama sebagai HTML atau JSON, dalam Bahasa Inggris atau Spanyol, terkompresi atau tidak. Negosiasi konten adalah mekanisme klien dan server menyepakati yang terbaik. Bentuk dominannya adalah negosiasi didorong server (proaktif): klien mencantumkan preferensinya di header request Accept*, dan server memilih kecocokan terbaik yang bisa ia hasilkan, menandakan pilihannya di header respons Content-* yang berpasangan. (Ada juga negosiasi didorong agen, tempat server mengembalikan daftar pilihan untuk dipilih klien, tapi jarang dipakai.)
Accept* -> server match ->
Content-*, drives format, language, and compression. Only the header name changes.
| Klien bilang (request) | Yang dinegosiasikan | Server menjawab (respons) |
|---|---|---|
Accept: application/json |
Tipe media / format | Content-Type |
Accept-Language: es |
Bahasa alami | Content-Language |
Accept-Encoding: gzip, br |
Kompresi | Content-Encoding |
Preferensi bisa diberi bobot dengan quality value 0 sampai 1: Accept: application/json;q=1.0, text/html;q=0.8, */*;q=0.1 berbunyi "sangat prefer JSON, terima HTML, dan sebagai jalan terakhir apa pun". Server menghormati tipe berbobot tertinggi yang bisa ia hasilkan. Jika tidak ada satu pun tipe yang bisa ia penuhi, respons yang benar adalah 406 Not Acceptable, bukan diam-diam mengembalikan format yang salah.
Kompresi: negosiasi yang sama, diterapkan ke ukuran
Kompresi hanyalah negosiasi encoding. Saat klien mengiklankan Accept-Encoding: gzip, server boleh mengompresi respons teks sebelum mengirim, dan penghematan pada JSON, HTML, dan JS dramatis karena semuanya sangat berulang: payload JSON ±26 MB bisa menyusut ke ±3,8 MB. Server men-set Content-Encoding: gzip, browser mendekompresi secara transparan, dan kode aplikasi di kedua ujung tidak menyadari apa pun. Dua aturan yang penting: hanya kompres saat klien bilang ia bisa mendekode (kalau tidak, Anda mengirim sampah), dan selalu pasangkan dengan Vary: Accept-Encoding supaya cache bersama tidak menyajikan body terkompresi ke klien yang tidak bisa menanganinya (bagian 12).
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(bigPayload)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Vary", "Accept-Encoding") // tell caches the body varies by encoding
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gz.Write(body)
return
}
w.Write(body) // uncompressed fallback for clients that can't decode gzip
}
from fastapi.middleware.gzip import GZipMiddleware
from fastapi import Header
# gzip responses for capable clients, but only above a size threshold
# (compressing tiny payloads costs more CPU than it saves bytes).
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/greeting")
def greeting(accept_language: str = Header(default="en")):
lang = "es" if accept_language.startswith("es") else "en"
return {"en": "Hello", "es": "Hola"}[lang]
Range Request
Range request meminta hanya sebagian dari sebuah resource, bukan semuanya. Ini memecahkan tiga masalah nyata: melanjutkan unduhan yang gagal di 80% tanpa mulai dari nol, seeking di audio/video (lompat ke menit ke-3 hanya mengambil byte di sekitarnya), dan memparalelkan unduhan besar menjadi potongan yang diambil bersamaan. Tanpa range, setiap interupsi berarti mulai ulang dan setiap seek berarti mengunduh semua yang ada sebelum target.
Pertukarannya, langkah demi langkah
- Server memberi sinyal dukungan range dengan menyertakan
Accept-Ranges: bytespada respons normal. - Klien meminta irisan dengan header
Range:Range: bytes=0-1023(kilobyte pertama), ataubytes=1024-(dari byte 1024 sampai akhir, persis yang dikirim unduhan yang dilanjutkan). - Server membalas 206 Partial Content dengan header
Content-Range: bytes 1024-50000/50001(irisan mana, dari total berapa) dan hanya byte itu di body. - Jika range yang diminta tidak valid (mis. mulai melewati akhir file), server mengembalikan 416 Range Not Satisfiable.
- Untuk melanjutkan, file mungkin berubah sejak unduhan parsial dimulai, jadi klien menambahkan
If-Range: "<etag>": server mengirim parsial hanya jika resource tidak berubah, kalau tidak ia mengembalikan 200 penuh supaya klien mulai bersih, bukan menjahit dua bagian yang tidak cocok.
# client resumes a download
GET /big.zip HTTP/1.1
Range: bytes=1024-
# server returns only the remaining slice
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 1024-50000/50001
Content-Length: 48977
Kabar baik bagi implementor: helper penyajian file standar di kedua ekosistem menangani semua ini, parsing range, 206, 416, dan If-Range, secara otomatis. Anda jarang perlu menulisnya manual.
// http.ServeContent handles Range, 206, 416 and If-Range for you,
// driven by the file's modtime and an optional ETag.
func download(w http.ResponseWriter, r *http.Request) {
f, _ := os.Open("big.zip")
defer f.Close()
info, _ := f.Stat()
http.ServeContent(w, r, "big.zip", info.ModTime(), f)
}
# Starlette's FileResponse honors Range requests and emits 206 automatically.
from fastapi.responses import FileResponse
@app.get("/big.zip")
def download():
return FileResponse("big.zip", media_type="application/zip")
Koneksi Persisten & Keep-Alive
Membangun koneksi TCP tidak gratis: ia memakan satu round trip penuh untuk handshake (lebih jika ada TLS). HTTP/1.0 membayar ongkos itu di setiap request, membuka dan merobohkan koneksi per resource, yang membuat memuat halaman berisi puluhan aset terasa sangat lambat. HTTP/1.1 memperbaikinya dengan koneksi persisten: satu koneksi TCP dipegang terbuka dan dipakai ulang untuk banyak siklus request/response, jadi ongkos handshake dibayar sekali dan diamortisasi ke semua yang menyusul.
Mekanikanya dan kenop-kenopnya
- Di HTTP/1.1, koneksi persisten sebagai bawaan: Anda dapat untungnya tanpa konfigurasi apa pun.
Connection: keep-alivesecara eksplisit meminta koneksi tetap terbuka, dan headerKeep-Alivebisa menyertakan parameter: timeout (berapa lama koneksi dipegang menganggur sebelum ditutup) dan max (berapa request yang dilayani sebelum ditutup).Connection: closememinta koneksi ditutup setelah respons ini, bawaan HTTP/1.0, masih bisa di-set di 1.1 saat Anda mau pertukaran sekali jalan.- Pipelining: mengirim beberapa request beruntun tanpa menunggu tiap respons, sempat dispesifikasikan di 1.1, tapi menderita head-of-line blocking (respons pertama yang lambat menahan sisanya di koneksi itu) dan begitu penuh bug di berbagai implementasi sampai praktis mati. Multiplexing HTTP/2 (bagian 2) adalah pengganti yang tepat.
Anda jarang meng-set header ini manual, bawaan-nya sudah benar untuk hampir semuanya. Koneksi persisten jadi perhatian sungguhan saat menyetel timeout server: memegang koneksi terlalu lama membocorkan koneksi idle dan menguras connection pool saat beban; menutupnya terlalu cepat menghilangkan untung latensi. Timeout di bawah juga berfungsi ganda sebagai pertahanan: timeout baca yang terlalu longgar membiarkan penyerang Slowloris mengikat koneksi dengan mengirim header satu byte demi satu byte.
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second, // how long to hold an idle keep-alive conn
ReadHeaderTimeout: 2 * time.Second, // mitigates Slowloris (slow-header attacks)
}
srv.ListenAndServe()
// srv.SetKeepAlivesEnabled(false) // force Connection: close if ever needed
# Keep-alive is owned by the ASGI server (uvicorn), not your app code:
# uvicorn main:app --timeout-keep-alive 60 --workers 4
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8080,
timeout_keep_alive=60, workers=4)
Upload Multipart & Respons Stream
JSON bukan tempat yang baik untuk data biner: Anda harus meng-encode base64, membengkak ±33% dan memaksa seluruhnya masuk memori. Transfer besar karena itu memakai mekanisme yang dibuat khusus, dan yang mana yang Anda pilih bergantung pada arah data besarnya.
Upload besar (klien -> server) -> multipart/form-data
Klien memecah body request menjadi bagian-bagian diskret: masing-masing bisa field form atau file, dipisahkan string delimiter unik yang dideklarasikan di parameter boundary dari Content-Type. Boundary muncul sebelum tiap bagian dan sekali lagi, dengan -- di belakangnya, untuk menandai bagian paling akhir. Ini membuat server bisa mem-parse body secara streaming, mengupas tiap bagian (dengan Content-Disposition-nya yang memberi nama dan Content-Type per bagian opsional) tanpa memuat semuanya ke memori sekaligus. Boundary yang unik inilah yang membuatnya aman menyematkan byte biner apa pun: parser mencari delimiter, tidak hanya mengandalkan panjang.
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----X7gQ2
------X7gQ2
Content-Disposition: form-data; name="file"; filename="cat.png"
Content-Type: image/png
(binary image bytes)
------X7gQ2-- # closing boundary marks the end
Unduhan besar (server -> klien) -> streaming
Untuk mengirim respons besar atau tanpa ujung yang jelas tanpa mem-buffer semuanya di memori (atau timeout di tengah membangunnya), server men-stream-nya dalam potongan. Dua teknik berkerabat: Transfer-Encoding: chunked mengirim body sebagai rangkaian chunk yang mendeskripsikan dirinya sendiri saat ukuran total belum diketahui di awal (jadi tidak ada Content-Length); dan Content-Type: text/event-stream mengimplementasikan Server-Sent Events, respons berumur panjang tempat server mendorong event data: berlabel begitu terjadi. Koneksi dipegang dengan keep-alive, dan klien menambahkan tiap chunk begitu tiba, ideal untuk update progres, tail log, atau keluaran LLM token demi token.
// 1. Receive a multipart upload
func upload(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(32 << 20) // up to 32 MB in memory, overflow spills to disk
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "no file", http.StatusBadRequest)
return
}
defer file.Close()
fmt.Fprintf(w, "received %s", header.Filename)
}
// 2. Stream a response in chunks (Server-Sent Events)
func stream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Connection", "keep-alive")
flusher := w.(http.Flusher) // Flush() pushes each chunk to the client immediately
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "data: chunk %d\n\n", i)
flusher.Flush()
time.Sleep(time.Second)
}
}
import asyncio
from fastapi import UploadFile
from fastapi.responses import StreamingResponse
# 1. Receive a multipart upload
@app.post("/upload")
async def upload(file: UploadFile):
data = await file.read() # UploadFile streams to a temp file under the hood
return {"received": file.filename, "size": len(data)}
# 2. Stream a response in chunks (Server-Sent Events)
@app.get("/stream")
async def stream():
async def gen():
for i in range(5):
yield f"data: chunk {i}\n\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
Mengirim data besar ke atas -> multipart/form-data. Menerima data besar ke bawah -> chunked transfer atau text/event-stream dengan keep-alive. Transfer parsial / dapat dilanjutkan -> range request (bagian 15). Real-time dua arah -> WebSocket (bagian 19).
Proxy, Tunneling & Header Forwarded
Di produksi aplikasi Anda hampir tidak pernah menghadap internet langsung. Ia berada di belakang reverse proxy atau load balancer, nginx, Envoy, HAProxy, load balancer cloud, yang mengakhiri TLS, membagi lalu lintas ke antar-instance, cache, dan routing. Ini bagus untuk operasional, tapi diam-diam mengubah apa yang dipersepsikan handler Anda tentang request, dan tidak memperhitungkannya menyebabkan satu keluarga bug yang berulang.
X-Forwarded-* headers below exist, and why trusting them
blindly is dangerous.Forward vs reverse proxy: dua ujung yang berlawanan
- Forward proxy berada di depan klien dan membuat request atas nama mereka, bayangkan gerbang keluar korporat, filter konten, atau anonimizer. Server tujuan melihat proxy-nya, bukan penggunanya. HTTPS lewat forward proxy memakai method
CONNECT(bagian 6) untuk membuka tunnel terenkripsi yang buram. - Reverse proxy berada di depan server; klien mengira mereka bicara langsung ke sana. Ia mengepalai backend Anda, menangani terminasi TLS, load balancing, caching, kompresi, dan routing berbasis path. Inilah yang kode Anda tinggali, dan CDN pada dasarnya adalah cache reverse-proxy yang tersebar global.
Konsekuensinya: klien asli hilang
Begitu reverse proxy meneruskan request ke aplikasi Anda, koneksi TCP yang dilihat handler Anda berasal dari IP proxy dan (biasanya) lewat HTTP polos, karena TLS diakhiri di tepi. Jadi r.RemoteAddr adalah proxy-nya, dan skemanya terlihat http meski pengguna di https. Untuk mengawetkan yang asli, proxy menyuntikkan header penerusan:
| Header | Membawa |
|---|---|
| X-Forwarded-For | IP klien asli, plus rantai proxy yang dilaluinya. |
| X-Forwarded-Proto | Skema asli, https, meski hop proxy->aplikasi http polos. |
| X-Forwarded-Host | Host yang semula diminta klien. |
| Forwarded | Header tunggal terstandar (RFC 7239) yang menggabungkan di atas. |
| Via | Proxy/gateway yang dilalui sebuah pesan, untuk deteksi loop dan tracing. |
Spoofing: X-Forwarded-For hanyalah header yang bisa di-set klien, jadi jika Anda memakainya untuk rate-limiting, geolokasi, atau log audit, percayai hanya saat request datang dari IP proxy tepercaya yang dikenal, kalau tidak penyerang memalsukan IP apa pun yang ia suka. URL skema salah: jika Anda membangun URL absolut atau redirect dari skema koneksi lokal (yang http di belakang proxy) alih-alih menghormati X-Forwarded-Proto, Anda akan membuat tautan http:// di situs HTTPS, memicu peringatan mixed-content dan loop redirect. Kebanyakan framework menyediakan middleware "trusted proxy" yang menulis ulang RemoteAddr dan skema dengan benar dari header ini; aktifkan itu daripada membaca header mentah.
Upgrade Protokol & WebSocket
Bentuk HTTP yang request/response dan diprakarsai klien bukan tempat yang pas untuk beban kerja yang butuh server mendorong data begitu sesuatu terjadi, notifikasi langsung, chat, state game multipemain, kursor kolaboratif, ticker trading. Dulu semua ini dipalsukan dengan polling (bertanya berulang) atau long-polling (menahan satu request terbuka). Mekanisme Upgrade HTTP/1.1 menawarkan jalan keluar yang bersih: koneksi HTTP yang sudah berdiri bisa berganti protokol di tengah aliran, paling terkenal menjadi WebSocket.
Handshake WebSocket
Yang elegan: ia mulai sebagai HTTP biasa, jadi melewati port dan proxy yang sama, lalu meng-upgrade:
- Klien mengirim GET normal yang membawa
Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-Keyacak, dan satu versi. - Server menyetujui dengan 101 Switching Protocols dan
Sec-WebSocket-Acceptyang dihitung dari key klien (membuktikan ia memahami handshake, bukan sekadar menggema). - Dari titik itu koneksi TCP bukan lagi HTTP, ia WebSocket persisten full-duplex yang membawa frame pesan ringan. Kedua sisi bisa mengirim kapan saja, tanpa overhead HTTP per pesan dan tanpa polling.
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Jangan meraih WebSocket secara refleks. Untuk stream satu arah server->klien, notifikasi, bar progres, tail log, token LLM yang mengalir, Server-Sent Events (bagian 17) lebih sederhana: ia HTTP polos, auto-reconnect, dan tidak butuh protokol khusus. Pakai WebSocket hanya saat Anda benar-benar butuh pesan dua arah berlatensi rendah (chat, game, edit kolaboratif). Dan untuk API request/response biasa, jangan keduanya, HTTP polos dengan caching yang baik adalah jawabannya.
SSL, TLS & HTTPS
Inilah lapisan yang mengubah HTTP menjadi HTTPS. Anda jarang mengimplementasikannya sendiri, tapi wajib paham apa yang ia jamin dan di mana ia berjalan.
- SSL (Secure Sockets Layer), protokol asli untuk mengamankan komunikasi klien-server. Semua versinya kini usang dan dinonaktifkan karena kerentanan yang ditemukan; namanya bertahan hanya dalam percakapan sehari-hari.
- TLS (Transport Layer Security), penerus modern SSL. Ia melakukan dua hal: mengenkripsi data saat transit sehingga penyadap tidak bisa membacanya dan pengubah tidak bisa diam-diam mengubahnya, dan memakai sertifikat untuk mengautentikasi bahwa server benar-benar yang ia klaim. Versi yang direkomendasikan saat ini TLS 1.3, yang lebih cepat sekaligus bersih dari cipher lemah warisan lama.
- HTTPS: sederhananya HTTP yang dibawa di dalam koneksi TLS. Protokol sama, semantik sama; semua di manual ini berlaku tanpa perubahan, hanya dengan enkripsi di bawahnya.
Mengalahkan serangan man-in-the-middle (MitM)
Serangan man-in-the-middle terjadi saat seseorang berada di jalur jaringan antara klien dan server, misalnya dengan menguasai router Wi-Fi publik, lalu mencegat lalu lintas. Di HTTP polos penyerang itu bertingkah seperti kurir pos yang tidak jujur: bisa membaca kata sandi di transit, memodifikasi body request, dan menulis ulang respons server, sementara kedua sisi mengira mereka bicara langsung satu sama lain.
Inilah persis kerentanan yang ditutup TLS. Begitu handshake di bawah selesai, setiap byte yang meninggalkan klien disegel di dalam kanal terenkripsi sebelum menyentuh kabel. Penyerang di jaringan yang dikompromikan masih melihat lalu lintas, hanya saja berupa aliran ciphertext yang tak terbaca, karena ia tidak pernah mendapat session key yang dinegosiasikan saat handshake. Tanpa key itu, penyadapan sia-sia: tidak ada yang bisa dibaca, dan tidak ada yang bisa diubah tanpa merusak pemeriksaan integritas yang diverifikasi penerima.
Apa yang sebenarnya dicapai handshake
Tanpa tenggelam dalam kriptografi, handshake TLS membangun tiga hal sebelum HTTP mengalir:
- Autentikasi: server menyajikan sertifikat yang ditandatangani Certificate Authority (CA) yang sudah dipercaya klien, membuktikan secara kriptografis bahwa ia punya
example.com. Inilah yang menghentikan Anda terhubung ke peniru. (Sertifikat menautkan naik ke sejumlah kecil root CA yang tertanam di OS/browser Anda.) - Pertukaran key: memakai kriptografi asimetris (public-key), kedua sisi menyepakati session key simetris bersama tanpa pernah mengirimkannya dalam bentuk terbuka. Kripto asimetris lambat, jadi ia hanya dipakai untuk bootstrap; key simetris yang cepat mengenkripsi lalu lintas sesungguhnya.
- Enkripsi + integritas: sejak saat itu setiap byte terenkripsi dan membawa pemeriksaan bukti-perubahan, jadi penyerang tidak bisa membaca maupun mengubah percakapan tanpa terdeteksi.
Detil praktis yang pantas diketahui: SNI (Server Name Indication) adalah bagian handshake tempat klien menyebut hostname yang ia mau, jadi satu IP yang melayani banyak situs HTTPS bisa menyajikan sertifikat yang benar, padanan lapisan TLS dari header Host.
Anda hampir tidak pernah mengimplementasikan TLS di kode aplikasi. Ia di-terminasi di tepi, oleh platform hosting, load balancer, atau reverse proxy (nginx, Caddy, LB cloud), sering dengan sertifikat yang disediakan dan diperbarui otomatis via Let's Encrypt. Aplikasi Anda lalu menerima HTTP polos di belakang batas itu. Inilah persis kenapa X-Forwarded-Proto (bagian 18) penting: dari sanalah aplikasi tahu pengguna di HTTPS, dan kenapa Anda men-set Strict-Transport-Security (bagian 5) di tepi untuk memaksa HTTPS sejak awal.
// Serve HTTPS directly with a certificate + private-key pair.
err := http.ListenAndServeTLS(":443", "server.crt", "server.key", mux)
if err != nil {
log.Fatal(err)
}
// Production note: usually you terminate TLS at a proxy/LB and run plain HTTP behind it.
# uvicorn can serve TLS directly with a cert + key pair:
# uvicorn main:app --port 443 --ssl-certfile server.crt --ssl-keyfile server.key
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=443,
ssl_certfile="server.crt", ssl_keyfile="server.key")
# Production note: usually terminate TLS at a proxy/LB and run plain HTTP behind it.
Crib-Sheet Debugging
Bayaran memahami mesin di baliknya adalah triase cepat: petakan gejala langsung ke subsistem dan header yang mengaturnya.
| Gejala | Periksa |
|---|---|
| Error CORS di console (tapi log server menampilkan 200) | Bandingkan Origin vs Access-Control-Allow-Origin; pastikan preflight OPTIONS mengembalikan Allow-Methods/Headers yang benar; untuk cookie, periksa Allow-Credentials dan origin bukan *. (bagian 9) |
| Data basi yang disajikan | Periksa Cache-Control, ETag/If-None-Match, Last-Modified; apakah ETag dinaikkan di setiap perubahan? Apakah Vary di-set benar? (bagian 12) |
| Lost update / penimpaan senyap | Tambah penguncian optimistis: wajibkan If-Match dengan ETag, tolak ketidakcocokan dengan 412. (bagian 13) |
| Record ganda saat pengulangan | POST tidak idempotent, wajibkan Idempotency-Key dan dedup berdasarkan itu. (bagian 7) |
| POST yang di-redirect kehilangan body | Anda memakai 301/302 (klien bisa pindah ke GET); pakai 307/308 untuk mengawetkan method & body. (bagian 11) |
| Format / bahasa yang keliru dikembalikan | Bandingkan Accept, Accept-Language, Accept-Encoding dengan Content-* server; pertimbangkan 406. (bagian 14) |
| Kebingungan 401 vs 403 | 401 = belum terautentikasi (kirim kredensial); 403 = terautentikasi tapi tidak diizinkan. (bagian 10) |
| 400 vs 422 | 400 = sintaks salah bentuk; 422 = sintaks valid, validasi bisnis gagal. (bagian 10) |
| 502 / 503 / 504 | Infrastruktur, bukan handler Anda: upstream tidak terjangkau / mati / lambat. Periksa health proxy dan upstream. (bagian 10, bagian 18) |
| Header yang Anda set tidak terkirim | Anda men-set-nya setelah menulis status atau body. Selalu set header lebih dulu. (bagian 3) |
| IP klien asli tampil sebagai proxy | Baca X-Forwarded-For, tapi hanya saat request datang dari proxy tepercaya. (bagian 18) |
| Situs HTTPS menghasilkan tautan http:// | Bangun URL absolut dari X-Forwarded-Proto, bukan skema lokal (yang ter-proxy). (bagian 18) |
| Koneksi habis saat beban | Setel timeout idle keep-alive dan batas koneksi; waspadai Slowloris via timeout baca header. (bagian 16) |
Header yang pantas dihafal, dikelompokkan per tugas
- Negosiasi:
Accept,Accept-Language,Accept-Encoding<->Content-Type,Content-Language,Content-Encoding - Representasi:
Content-Type,Content-Length,Content-Range - Identitas / auth:
Authorization,WWW-Authenticate,Cookie,Set-Cookie,Host,User-Agent - Caching / kondisi:
Cache-Control,ETag,Last-Modified,If-None-Match,If-Match,If-Modified-Since,Vary - CORS:
Origin,Access-Control-Allow-Origin / -Methods / -Headers / -Credentials / -Max-Age - Keamanan:
Strict-Transport-Security,Content-Security-Policy,X-Frame-Options,X-Content-Type-Options - Proxy:
X-Forwarded-For / -Proto / -Host,Forwarded,Via - Koneksi / transfer:
Connection,Keep-Alive,Upgrade,Transfer-Encoding,Range,Accept-Ranges
Seluruh alur dalam satu tarikan napas: klien membuka koneksi, bisa dipakai ulang (bagian 16), biasanya terenkripsi TLS (bagian 20), sering lewat reverse proxy (bagian 18), dan mengirim request yang mandiri (bagian 1): method dengan semantik safe/idempotent/cacheable (bagian 6), URL, header (bagian 4-5), dan body opsional (bagian 3). Server yang stateless membangun ulang identitas (bagian 8), memeriksa otorisasi dan prasyarat (bagian 10, bagian 13), menegosiasikan representasi terbaik (bagian 14), berkonsultasi ke cache (bagian 12), melakukan pekerjaannya, dan mengembalikan respons berstatus code (bagian 10), mungkin redirect (bagian 11), range parsial (bagian 15), atau stream (bagian 17). Internalisasi lingkaran itu dan header yang mengarahkan tiap langkahnya, dan Anda bisa menalar serta men-debug mayoritas HTTP backend yang akan Anda temui.
Berpijak pada MDN (developer.mozilla.org/en-US/docs/Web/HTTP), contoh RFC 9110, Go 1.22+, dan Python 3.11+.