Handler & middleware
Middleware di Go bukan fitur framework. Ia cuma fungsi yang menerima Handler dan mengembalikan Handler — dan karena semua orang memakai tipe yang sama, middleware dari pustaka mana pun bisa dipasang di server mana pun.
Intisari
Handler= satu methodServeHTTP(w, r).HandlerFuncmengubah fungsi biasa jadi Handler.- Middleware berbentuk
func(http.Handler) http.Handler. Itu satu-satunya konvensi yang perlu disepakati. - Urutan pemasangan menentukan urutan eksekusi: yang terluar dipasang terakhir, dan jalan pertama.
- Middleware yang menulis respons harus
returntanpa memanggilnext. - Untuk membaca status respons di middleware log, kamu perlu membungkus
ResponseWriter— dan bungkusnya wajib meneruskanFlushdanHijack.
Interface-nya
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
// Adaptor supaya fungsi biasa memenuhi interface itu
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
Empat baris terakhir itu adalah salah satu potongan kode paling berpengaruh di Go: ia membuat fungsi biasa,
struct dengan method, router, FileServer, dan setiap framework pihak ketiga semuanya menjadi
hal yang sama di mata server.
Bentuk middleware
func Log(log *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mulai := time.Now()
ww := &penulisTerbungkus{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(ww, r) // ← sebelum baris ini: pra-proses
log.InfoContext(r.Context(), "permintaan",
"metode", r.Method,
"path", r.URL.Path,
"status", ww.status,
"byte", ww.byte,
"durasi_ms", time.Since(mulai).Milliseconds())
})
}
}
Bentuk func(http.Handler) http.Handler ini adalah konvensi de facto seluruh ekosistem.
Karena semua sepakat, middleware dari chi bisa dipakai di server ServeMux polos, dan sebaliknya.
Membungkus ResponseWriter
type penulisTerbungkus struct {
http.ResponseWriter
status int
byte int
tertulis bool
}
func (w *penulisTerbungkus) WriteHeader(kode int) {
if w.tertulis {
return // cegah "superfluous WriteHeader"
}
w.status, w.tertulis = kode, true
w.ResponseWriter.WriteHeader(kode)
}
func (w *penulisTerbungkus) Write(b []byte) (int, error) {
if !w.tertulis {
w.WriteHeader(http.StatusOK)
}
n, err := w.ResponseWriter.Write(b)
w.byte += n
return n, err
}
// WAJIB: tanpa ini, SSE dan streaming berhenti bekerja.
func (w *penulisTerbungkus) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// WAJIB: tanpa ini, upgrade WebSocket gagal.
func (w *penulisTerbungkus) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, errors.New("tidak mendukung hijack")
}
Ini jebakan yang mahal. http.ResponseWriter yang asli juga memenuhi
Flusher, Hijacker, dan ReaderFrom. Begitu kamu membungkusnya
dengan struct yang hanya punya Write dan WriteHeader, semua kemampuan tambahan
itu hilang — dan gejalanya jauh dari penyebabnya: streaming yang menumpuk di buffer, WebSocket
yang gagal upgrade, atau io.Copy berkas besar yang tiba-tiba jadi lambat. Kalau kamu tidak
ingin menulis ini sendiri, pakai chi/middleware.NewWrapResponseWriter.
Urutan pemasangan
handler := Pulihkan( // ← jalan PERTAMA, selesai TERAKHIR
RequestID(
Log(logger,
BatasWaktu(10*time.Second,
CORS(
mux, // ← handler sebenarnya
)))))
masuk ► Pulihkan ► RequestID ► Log ► BatasWaktu ► CORS ► handler
keluar ◄ Pulihkan ◄ RequestID ◄ Log ◄ BatasWaktu ◄ CORS ◄
| Urutan | Middleware | Kenapa di sana |
|---|---|---|
| 1 | Pemulih panic | Harus membungkus semuanya, termasuk middleware lain |
| 2 | Request ID / trace | Supaya semua log setelahnya membawa ID yang sama |
| 3 | Log akses | Mencatat semua permintaan, termasuk yang ditolak |
| 4 | Timeout permintaan | Batas waktu terluar untuk seluruh pemrosesan |
| 5 | CORS, kompresi, header keamanan | Berlaku untuk semua respons |
| 6 | Rate limit | Sebelum autentikasi — biar penyerang tidak bisa membebani pengecekan token |
| 7 | Autentikasi | Sedekat mungkin dengan handler |
| 8 | Otorisasi per rute | Di dalam grup rute yang bersangkutan |
Rate limit di atas autentikasi, bukan di bawahnya. Kalau autentikasi jalan duluan, penyerang yang membanjiri endpoint dengan token palsu tetap memaksa servermu melakukan verifikasi bcrypt atau JWT untuk setiap permintaan — pekerjaan mahal yang persis ingin dihindari. Dibahas lagi di Fase 6.
Merantai tanpa tanda kurung bertingkat
type Rantai []func(http.Handler) http.Handler
func (c Rantai) Bungkus(h http.Handler) http.Handler {
for i := len(c) - 1; i >= 0; i-- { // dari dalam ke luar
h = c[i](h)
}
return h
}
handler := Rantai{
Pulihkan,
RequestID,
Log(logger),
BatasWaktu(10 * time.Second),
}.Bungkus(mux)
Middleware yang menghentikan permintaan
func (h *Handler) WajibLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := h.auth.DariRequest(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return // ← WAJIB. Tanpa ini handler tetap jalan.
}
// Teruskan identitas lewat context, bukan lewat variabel global.
ctx := auth.DenganPengguna(r.Context(), u)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
return yang hilang di sini adalah lubang autentikasi. Tanpa itu, permintaan yang
ditolak tetap diteruskan ke handler — yang kemudian menulis respons kedua di atas 401. Statusnya
tetap 401 (yang pertama menang), sehingga di log semuanya terlihat normal, sementara handler-mu
benar-benar mengeksekusi aksi yang seharusnya ditolak. Tulis tesnya, jangan andalkan mata (Fase 8).
Middleware yang layak dipunyai sejak awal
- Pemulih panic — dengan stack trace dan penghitung untuk alarm.
- Request ID — dibuat kalau belum ada, dikembalikan lewat header, ikut ke setiap baris log.
- Log akses terstruktur — metode, path (pola rute, bukan URL mentah), status, durasi, ukuran.
- Timeout — memasang tenggat pada
r.Context(). - Header keamanan —
X-Content-Type-Options,Referrer-Policy, CSP (Fase 6). - Batas ukuran body —
http.MaxBytesReaderuntuk semua metode yang menerima body.
Catat pola rute, bukan URL mentah. GET /produk/{id} menghasilkan metrik
yang bisa dijumlahkan; GET /produk/58213 menghasilkan satu juta label unik yang akan
meledakkan biaya CloudWatch dan membuat dasbormu tidak berguna. Ini muncul lagi di Fase 9.
Latihan: tulis middleware Log lengkap dengan pembungkus ResponseWriter,
pasang di server, dan pastikan status 404 tercatat benar. Lalu hapus method Flush() dari
pembungkusmu, buat endpoint yang mengirim server-sent events, dan buktikan datanya berhenti
mengalir — itu bug yang akan sangat sulit kamu temukan kalau tidak pernah melihatnya sekali.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.