A detailed backend reference

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.

Layer 7 / Application TCP / QUIC Go 1.22+ / Python 3.11+ 21 sections Versi Sederhana
Part I / Foundations
01

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.

Where HTTP lives
The protocol stack, HTTP rides on top of everything below it
YOU WORK HERE Application, HTTP methods / headers / status codes / the meaning of a message L7 TLS (optional), encryption + authentication turns HTTP into HTTPS / sec 20 Transport, TCP (or QUIC/UDP for HTTP/3) the one requirement HTTP makes: reliable, ordered delivery L4 Network, IP / packets & routing L3 Link, Ethernet / Wi-Fi / the actual wire L2/1 Everything below the top is abstracted away as "a reliable, possibly encrypted pipe."

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.

Each request is like a letter that has to re-introduce who you are, every single time, the postal service never remembers your last envelope, so every letter must restate the sender, recipient, and return address.

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.
Statelessness, visualized
Any request can land on any server, because each carries its own context
Client 3 requests Load Balancer Server A no memory of you Server B no memory of you Server C no memory of you identical & interchangeable
No server owns the conversation, so the load balancer is free to scatter requests, and if a node dies, the client just retries elsewhere.

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).
Before a single byte of HTTP
The TCP 3-way handshake, and the round trip it costs
Client Server <-- one full round trip of latency, before any data --> SYN "let's talk, here's my sequence number" SYN-ACK "got it, and here's mine" ACK connection established GET /index.html <- finally, HTTP 200 OK
This handshake cost, paid once per connection, is exactly why HTTP/1.1's persistent connections (sec 16) and HTTP/3's QUIC (sec 2) work so hard to avoid repeating it.

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.
02

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.

Three decades
Same semantics, evolving plumbing
0.9 1991 one line: GET /page 1.0 1996 headers, status codes, POST 1 conn / request 1.1 1997 keep-alive, Host, chunked encoding dominant ~20 yrs 2 2015 multiplexing, binary, HPACK 3 2022 QUIC over UDP drops TCP Each version only optimizes connection reuse, parallelism, compression, and transport, never the meaning of a message.
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
HTTP/1.1 serial queue vs HTTP/2 multiplexed streams
HTTP/1.1, one connection, serial req A req B, slow! everyone waits req C req D <-- C & D blocked behind B time ------------------------------------------------> HTTP/2, one connection, interleaved streams ABCD A, C, D finish early, B's slowness blocks nobody
HTTP/2 chops every response into frames and interleaves them on one connection, so a slow stream no longer holds the others hostage.

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).

The one takeaway

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.

03

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.

One skeleton, two messages
The four parts every HTTP message shares
REQUEST POST /api/v1/notes HTTP/1.1 Host: api.example.com Authorization: Bearer eyJ... Content-Type: application/json Content-Length: 33 (blank line) {"title":"buy milk","done":false} RESPONSE HTTP/1.1 201 Created Content-Type: application/json Content-Length: 41 Date: Sat, 30 May 2026 ... (blank line) {"id":42,"title":"buy milk",...} 1 / start line method/target/version / version/code/phrase 2 / headers case-insensitive key: value metadata 3 / blank line the hard boundary, "headers end, body begins" 4 / body (optional) the payload, typed by Content-Type
Content-Length tells the receiver exactly how many body bytes to read before the next message begins on a persistent connection, get it wrong and the whole connection desyncs.

Pesan request, baris demi baris

raw HTTP request
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

raw HTTP response
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).
Universal ordering rule

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
04

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?

A shipping parcel writes the recipient, address, and PIN code on the outside label, not sealed inside the box. Every courier who handles it along the route reads the routing info at a glance, without tearing the box open. If that data were inside, each handler would have to unpack and repack the parcel just to know where it's going next. HTTP headers are that outer label: metadata any intermediary (proxy, cache, gateway) can read and act on without touching the payload.
Label vs contents
Why metadata rides on the outside
> HEADERS, the label Host: api.example.com Cache-Control: max-age=3600 Authorization: Bearer ... > BODY, sealed contents { ... the actual payload ... } Proxy Cache Gateway each reads the label without opening the box

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.

05

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

One header, one threat
Each security header is a countermeasure to a named attack
HEADER (server sets) ATTACK BLOCKED Strict-Transport-Security "only ever reach me over HTTPS" protocol downgrade Content-Security-Policy allow-list of script/style sources XSS (script injection) X-Frame-Options: DENY forbid embedding in an iframe clickjacking X-Content-Type-Options nosniff, don't guess the type MIME sniffing Set-Cookie: HttpOnly; Secure hide cookie from JS, HTTPS-only session theft

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: DENY melarang 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: nosniff menghentikan browser menebak ("sniffing") tipe sebuah resource saat Content-Type yang dideklarasikan terlihat salah. Sniffing bisa dikelabui untuk menjalankan gambar yang di-upload sebagai skrip, itulah serangan MIME-sniffing klasik, dan header ini mematikannya.
  • Set-Cookie dengan HttpOnly dan Secure. HttpOnly menyembunyikan cookie dari JavaScript (jadi XSS yang berhasil tidak bisa mencuri sesi), dan Secure memastikan 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)
Part II / Methods & Semantics
06

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.
How the properties nest
Every safe method is idempotent, but not the reverse
IDEMPOTENT, safe to retry SAFE, read-only CACHEABLE GET HEAD PUT DELETE OPTIONS TRACE POST neither, needs a key sec 7 PATCH usually neither outside every circle not safe to blindly retry
PUT and DELETE are idempotent but not safe, they change state, just deterministically to the same end result no matter how many times you repeat them.
How the properties relate

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.

GETRetrieve a representation of a resource safeidempotentcacheable

"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).
HEADLike GET, but headers only, no body safeidempotentcacheable

"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.

POSTSubmit data; create a resource or trigger processing not safenot idempotentcond. cacheable

"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 Location yang 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.
PUTReplace the target resource entirely with the body not safeidempotentnot cacheable

"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).

PUT vs POST for creation

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.

PATCHApply a partial modification to a resource not safenot idempotent*cond. cacheable

"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.

Rule of thumb

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.

DELETERemove the target resource not safeidempotentnot cacheable

"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.

OPTIONSDescribe the communication options for the target safeidempotentnot cacheable

"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.

CONNECTEstablish a tunnel through a proxy not safenot idempotentnot cacheable

"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).

TRACELoop-back diagnostic along the request path safeidempotentnot cacheable

"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): ...
07

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.

The bug a key prevents
Why a retried POST double-charges
Client Server POST /charge ($50) charged once 201 ... response lost, client never learns it worked POST /charge ($50) <- retried! charged AGAIN $100 charged for one $50 purchase
The server did nothing wrong each time, POST simply isn't idempotent, so the retry is indistinguishable from a genuine second purchase. An idempotency key closes that gap.
The fix: idempotency keys

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.)

1
Client sends POST /payments with Idempotency-Key: 9f8c-....
v
2
Server checks its key store. Key unseen -> run the charge, persist key -> result, return 201.
v
3
The network drops the response. The client, unsure, retries with the same key.
v
4
Server sees the key already exists -> returns the stored result. No second charge.

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.

Part III / State, Identity & Cross-Origin
08

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).

cookie round-trip
# 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.cookie tidak 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.
  • SameSite mengendalikan apakah cookie ikut pada request lintas-situs, pertahanan inti terhadap CSRF (cross-site request forgery). Strict tidak pernah mengirimnya lintas-situs; Lax (bawaan modern) mengirimnya pada navigasi tingkat atas tapi tidak pada sub-request lintas-situs; None selalu mengirimnya tapi lalu wajib Secure.
  • 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
Anatomy of a token
A JWT is three base64url parts joined by dots
eyJhbGciOiJIUzI1... . eyJzdWIiOiI0Mi... . SflKxwRJSMeKKF... 1 / HEADER { "alg": "HS256",   "typ": "JWT" } names the signing algorithm readable by anyone 2 / PAYLOAD { "sub": 42,   "exp": 1717...,   "role": "user" } the claims, user id, expiry, roles encoded, NOT encrypted 3 / SIGNATURE HMAC(header+payload,      secret) proves the token wasn't forged or tampered with only the key-holder can forge
Anyone can read a JWT, but only the holder of the secret/private key can produce a valid signature, which is what lets a server trust it with no database lookup.

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.

Encoded is not encrypted

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}
09

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.

What counts as "same origin"
Change any of scheme / host / port -> it's a different origin
BASE https://example.com scheme / host / :443 https://example.com/api/users SAME, only path differs http://example.com different SCHEME https://api. example.com different HOST (subdomain) https://example.com:8443 different PORT
The crucial framing

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.

1
Browser automatically adds Origin: https://example.com and sends the request.
v
2
Server checks the origin against its policy; if allowed, it includes Access-Control-Allow-Origin echoing that origin (or *) in the response.
v
3
Browser sees a matching header -> hands the response to the page. Header missing -> browser blocks the response and logs a CORS error, even though the server processed the request.

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 Authorization atau header X- khusus apa pun.
  • Content-Type-nya di luar himpunan sederhana, dan application/json memang di luarnya.
Why you see OPTIONS everywhere

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.

1
Browser sends OPTIONS /resource with Access-Control-Request-Method and -Request-Headers, asking "may I send this method with these headers from my origin?", no body.
v
2
Server replies 204 No Content with 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.
v
3
Approval granted -> the browser sends the actual request and returns the real response to the page.

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.

Who protects what

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.

Part IV / Responses, Caching & Concurrency
10

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.

Read the first digit
Five classes, the leading digit assigns blame at a glance
1xx Informational "received, continue" 2xx Success "it worked" 3xx Redirection "look elsewhere" 4xx Client error YOUR fault (caller) 5xx Server error SERVER's fault no blame no blame caller broke it server broke it A 4xx in your logs is the client's mistake; a 5xx is yours.
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 vs 403, the most common mix-up

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 vs 422

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 and the proxy trio

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
11

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.

Method preservation
Send a POST + body at each redirect, what comes out the other side?
CODE SENT RE-ISSUED AS LIFE 301 POST + body GET / body dropped* permanent 302 POST + body GET / body dropped* temporary 303 POST + body GET (always, by design) temporary 307 POST + body POST + body preserved temporary 308 POST + body POST + body preserved permanent The split that matters: 307 / 308 are the only codes that guarantee your POST + body survive. 301 / 302 may silently downgrade POST -> GET and drop the body, the historical quirk that forced 307/308 into existence. 303 downgrades to GET on purpose, exactly what Post/Redirect/Get wants. * "may", clients historically did this on POST even though the spec didn't sanction it.
Reading top to bottom: the safe modern defaults for a state-changing redirect are at the bottom, reach for 307/308 (preserve) or 303 (deliberately drop), never the ambiguous 301/302.
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.
The historical trap that explains 307/308

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
12

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.
The two layers as one decision
A cached response runs this gauntlet, each gate avoids more work than the next
New request for a resource LAYER 1 Still fresh? max-age not expired YES Serve from cache zero network / 0 bytes NO LAYER 2 Revalidate If-None-Match: ETag 304 Not Modified reuse copy / empty body round trip, payload saved changed 200 OK + new body full payload + fresh ETag COST climbs left -> right: 0 bytes -> tiny round trip -> full download
The whole game is to exit as far left as possible. Freshness wins the most (no network at all); validation is the cheap fallback; a full re-download is the last resort.

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.
  • public vs private: bolehkah cache bersama (CDN/proxy) menyimpannya, atau hanya browser pengguna sendiri? Pakai private untuk 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.

1
First GET -> 200 + body + ETag: "abc" + Cache-Control: max-age=10.
v
2
After freshness expires, the next GET sends If-None-Match: "abc" (and/or If-Modified-Since).
v
3a
Unchanged -> 304 Not Modified with an empty body. The client reuses its copy; the payload bytes are saved.
3b
Changed -> 200 with the new body and a fresh ETag.

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"})
In practice + the Vary trap

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.

13

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.

The problem it solves
The lost update, and how one If-Match turns silent loss into a catchable 412
WITHOUT, SILENT LOSS Alice Bob reads v1 reads v1 too saves -> server now v2 saves (from v1)... Bob overwrites v2 -> Alice's work gone no error / no warning / 200 OK WITH If-Match, CONFLICT CAUGHT Alice Bob reads v1 reads v1 too If-Match:v1 -> v2 If-Match:v1, but it's v2 now 412 Precondition Failed -> Bob refetches reapply on v2 / retry / nothing lost
Optimistic locking doesn't prevent the race, it makes the race detectable. The precondition turns a silent overwrite into an explicit 412 the client can recover from.

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

1
Client GETs /doc/7 -> receives the body plus ETag: "v1".
v
2
After editing, client sends PUT /doc/7 with If-Match: "v1", "only save if it's still v1, i.e. nobody changed it under me."
v
3a
Still v1 -> apply the update, return 200 + the new ETag: "v2".
3b
Someone already bumped it to v2 -> reject with 412 Precondition Failed. No lost update, the client refetches v2, reapplies its change, and retries.
Why this beats a database lock

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})
Part V / Data Transfer & Connections
14

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.)

Server-driven negotiation
The client states weighted preferences; the server picks the best it can actually produce
CLIENT SAYS Accept: application/json q=1.0 text/html q=0.8 */* q=0.1 SERVER CAN PRODUCE application/json text/html application/xml highest q it can serve wins RESPONSE Content-Type: application/json 200 OK If the server can satisfy none of the acceptable types -> the correct answer is 406 Not Acceptablenever silently return the wrong format.
The same three-step pattern, client 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]
15

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.

Resume, don't restart
A download dies at byte 1024, Range fetches only what's missing
big.zip 50,001 bytes total got: 0-1023 missing: 1024-50000connection dropped byte 1024 GET /big.zip Range: bytes=1024- "send me from 1024 to the end" 206 Partial Content Content-Range: bytes 1024-50000/50001 Content-Length: 48977 Accept-Ranges: bytes on a normal reply advertises support / invalid range -> 416 If-Range: "etag" guards a resume, if the file changed, server sends the full 200 instead of a mismatched slice.
Seeking in a video and parallel multi-connection downloads are the same trick: ask for the byte window you actually need, get a 206 back.

Pertukarannya, langkah demi langkah

  • Server memberi sinyal dukungan range dengan menyertakan Accept-Ranges: bytes pada respons normal.
  • Klien meminta irisan dengan header Range: Range: bytes=0-1023 (kilobyte pertama), atau bytes=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.
range exchange / resuming from byte 1024
# 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")
16

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.

Paying the handshake once
Three resources, two strategies, count the handshakes
HTTP/1.0, NEW CONNECTION EACH TIME (cycle) TCP handshake (1 RTT) GET /a -> close (cycle) TCP handshake (1 RTT) GET /b -> close (cycle) TCP handshake (1 RTT) GET /c -> close = 3 handshakes / 3 round trips wasted HTTP/1.1, ONE PERSISTENT CONNECTION (cycle) handshake (once) GET /a GET /b GET /c connection held open (idle timeout), reused for all three = 1 handshake / the cost is amortized across every request that follows
In HTTP/1.1 this is the default, you get it for free. The tuning question is purely how long to hold an idle connection open before reclaiming it.

Mekanikanya dan kenop-kenopnya

  • Di HTTP/1.1, koneksi persisten sebagai bawaan: Anda dapat untungnya tanpa konfigurasi apa pun.
  • Connection: keep-alive secara eksplisit meminta koneksi tetap terbuka, dan header Keep-Alive bisa menyertakan parameter: timeout (berapa lama koneksi dipegang menganggur sebelum ditutup) dan max (berapa request yang dilayani sebelum ditutup).
  • Connection: close meminta 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.
Where this matters for you

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)
17

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.

Direction decides the mechanism
Up: boundary-delimited parts  /  Down: a stream of chunks over one held-open connection
CLIENT -> SERVER / multipart/form-data Content-Type: ...; boundary=----X7 ------X7 Content-Disposition: name="title" buy milk ------X7 ...name="file"; filename="cat.png" Content-Type: image/png <binary image bytes> ------X7-- end marker parser scans for the boundary, binary-safe SERVER -> CLIENT / streamed chunks Content-Type: text/event-stream Connection: keep-alive (held open) time -> data: chunk 0 data: chunk 1 data: chunk 2 ... no Content-Length, pushed as produced
Upload streams in (each part peeled off without buffering the whole body); response streams out (each chunk flushed the moment it's ready), both avoid loading the full payload into memory.

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.

multipart request shape
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")
Direction -> tool

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).

Part VI / Security & Operations
18

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.

Opposite ends of the wire
A forward proxy fronts clients; a reverse proxy fronts servers
FORWARD PROXY, in front of clients client A client B forward proxy egress / filter / CONNECT the open internet sees the proxy, not the user hides the client REVERSE PROXY, in front of servers (this is where your code lives) clients (HTTPS) think they talk to it reverse proxy / LB TLS term / route / cache + X-Forwarded-* app instance 1 app instance 2 app instance 3 plain HTTP behind here
Because TLS is terminated at the reverse proxy, your handler sees the proxy's IP over plain HTTP, which is exactly why the 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.
Two traps that bite everyone

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.

19

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.

One connection, two lives
It begins as ordinary HTTP, then the 101 flips it into a full-duplex pipe
Client Server HTTP GET /ws / Upgrade: websocket / Sec-WebSocket-Key 101 Switching Protocols / Sec-WebSocket-Accept v no longer HTTP v WEBSOCKET frame -> "hello" <- frame "welcome" (server pushes anytime) <- frame "new message" frame -> "typing..." either side sends at will / no polling / no per-message HTTP overhead
Because it starts as a normal GET, the handshake sails through the same ports and proxies as any HTTP request, then sheds HTTP entirely once upgraded.

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-Key acak, dan satu versi.
  • Server menyetujui dengan 101 Switching Protocols dan Sec-WebSocket-Accept yang 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.
upgrade handshake
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=
Choosing the right tool

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.

20

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.

What the handshake buys
Three guarantees established before a single byte of HTTP flows
Client Server ClientHello / ciphers / SNI: example.com (1) AUTHENTICATION server sends a CA-signed certificate -> proves it owns the domain (2) KEY EXCHANGE asymmetric crypto agrees a shared symmetric session key, never sent in clear (3) ENCRYPTION + INTEGRITY every byte now encrypted & tamper-checked with the fast symmetric key HTTP now flows inside SNI lets one IP serving many HTTPS sites present the right certificate, the TLS-layer analogue of the Host header. Asymmetric crypto is slow, so it only bootstraps; the symmetric key does the real work.
TLS 1.3 compresses this into fewer round trips (often one, or zero on resumption), and in practice it all happens at the edge proxy, not in your app code.

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.

Where TLS actually lives in your stack

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.
21

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
The whole loop
Every request you'll ever debug walks this same path
client sec 1 request TLS sec 20 / proxy sec 18 keep-alive sec 16 the reusable pipe STATELESS SERVER identity / auth sec 8 authz + preconditions sec 10/sec 13 negotiate representation sec 14 consult caches sec 12 do the work, pick a method sec 6 method semantics: safe / idempotent / cacheable status-coded response sec 10-2xx / 3xx redirect sec 11 / 206 sec 15 / stream sec 17
Internalize this loop and the headers steering each step, and the overwhelming majority of backend HTTP becomes legible at a glance.

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+.