← Semua pembelajaran / Go Nol → Enterprise
Fase 3 · HTTP — Request sampai Response

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Handler = satu method ServeHTTP(w, r). HandlerFunc mengubah 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 return tanpa memanggil next.
  • Untuk membaca status respons di middleware log, kamu perlu membungkus ResponseWriter — dan bungkusnya wajib meneruskan Flush dan Hijack.

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 ◄
UrutanMiddlewareKenapa di sana
1Pemulih panicHarus membungkus semuanya, termasuk middleware lain
2Request ID / traceSupaya semua log setelahnya membawa ID yang sama
3Log aksesMencatat semua permintaan, termasuk yang ditolak
4Timeout permintaanBatas waktu terluar untuk seluruh pemrosesan
5CORS, kompresi, header keamananBerlaku untuk semua respons
6Rate limitSebelum autentikasi — biar penyerang tidak bisa membebani pengecekan token
7AutentikasiSedekat mungkin dengan handler
8Otorisasi per ruteDi 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

  1. Pemulih panic — dengan stack trace dan penghitung untuk alarm.
  2. Request ID — dibuat kalau belum ada, dikembalikan lewat header, ikut ke setiap baris log.
  3. Log akses terstruktur — metode, path (pola rute, bukan URL mentah), status, durasi, ukuran.
  4. Timeout — memasang tenggat pada r.Context().
  5. Header keamanan — X-Content-Type-Options, Referrer-Policy, CSP (Fase 6).
  6. Batas ukuran body — http.MaxBytesReader untuk 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.