B.29. HTTP Handler Context Value
Dalam arsitektur web server yang menggunakan middleware, ada kebutuhan umum untuk meneruskan data dari satu lapisan ke lapisan berikutnya tanpa harus mengubah signature fungsi handler. Misalnya, middleware autentikasi yang sudah memvalidasi user perlu meneruskan informasi user tersebut ke handler, atau middleware logging yang men-generate request ID perlu memastikan ID tersebut tersedia di seluruh chain.
Go menyediakan mekanisme untuk penanganan kasus ini melalui context yang tertanam di *http.Request. Context bisa diisi dengan data di middleware lalu dibaca kembali di handler tanpa parameter tambahan. Pada chapter ini kita akan belajar cara menggunakannya.
B.29.1. Konsep Context Value
Setiap *http.Request membawa sebuah context yang bisa diisi dengan data menggunakan context.WithValue(). Data tersebut kemudian bisa dibaca di handler lewat r.Context().Value(key).
// Di middleware: simpan data ke context
ctx := context.WithValue(r.Context(), key, value)
next.ServeHTTP(w, r.WithContext(ctx))
// Di handler: baca data dari context
value := r.Context().Value(key)
B.29.2. Context Typed Key
Penting untuk tidak menggunakan string biasa sebagai key context. Jika ada 2 package berbeda yang sama-sama menggunakan key "user", yang terjadi adalah value context dengan key "user" tersebut akan saling ditimpa.
Best practice-nya adalah menggunakan tipe custom, bisa dengan mendefinisikan tipe baru dari string atau int.
type contextKey string
const (
contextKeyUser contextKey = "user"
contextKeyRequestID contextKey = "request_id"
)
Tipe contextKey berbeda dari string, sehingga nilai key ini tidak akan pernah bentrok dengan key dari package lain.
B.29.3. Implementasi
Pada contoh berikut kita akan membuat web server dengan dua middleware: satu untuk autentikasi yang menyimpan data user ke context, dan satu untuk request ID yang digunakan untuk logging dan tracing. Handler membaca kedua nilai tersebut dari context untuk menyusun response dan log.
Tulis ini di main.go.
package main
import (
"context"
"log"
"net/http"
)
type contextKey string
const (
contextKeyUser contextKey = "user"
contextKeyRequestID contextKey = "request_id"
)
type User struct {
Username string
Role string
}
func middlewareAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, _, ok := r.BasicAuth()
if !ok || username == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
user := User{Username: username, Role: "admin"}
ctx := context.WithValue(r.Context(), contextKeyUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func middlewareRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = "auto-generated-001"
}
ctx := context.WithValue(r.Context(), contextKeyRequestID, requestID)
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handleProfile(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(contextKeyUser).(User)
if !ok {
http.Error(w, "user not found in context", http.StatusInternalServerError)
return
}
requestID, _ := r.Context().Value(contextKeyRequestID).(string)
log.Printf("[%s] profile accessed by %s (%s)", requestID, user.Username, user.Role)
w.Write([]byte("Hello, " + user.Username + " [" + user.Role + "]"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /profile", handleProfile)
handler := middlewareRequestID(middlewareAuth(mux))
log.Println("server started at localhost:9000")
err := http.ListenAndServe(":9000", handler)
if err != nil {
log.Fatal(err)
}
}
middlewareAuth() menggunakan r.BasicAuth() untuk membaca kredensial dari header Authorization. Jika valid, informasi user dibungkus ke dalam struct User lalu disimpan ke context via context.WithValue(). Alasan menggunakan struct bukan sekadar string adalah agar data yang dibawa lebih terstruktur dan bisa langsung digunakan tanpa parsing tambahan di handler.
middlewareRequestID() membaca header X-Request-ID dari request. Jika header tersebut tidak dikirim client, nilai fallback "auto-generated-001" digunakan sebagai gantinya. Request ID kemudian disimpan ke context sekaligus dikirim kembali ke client via response header X-Request-ID, sehingga client bisa menggunakannya untuk keperluan tracing.
Kedua middleware menggunakan pola r.WithContext(ctx) untuk meneruskan context yang sudah diperkaya ke handler berikutnya. r.WithContext() tidak memodifikasi request asli, melainkan mengembalikan salinan request baru dengan context yang diberikan.
Di main(), middleware dirantai dengan pola middlewareRequestID(middlewareAuth(mux)). Urutan nesting ini menentukan urutan eksekusi: middlewareRequestID() dieksekusi pertama, lalu middlewareAuth(), baru handler. Urutan ini penting karena jika dibalik, request ID belum ada di context saat auth berjalan.
B.29.4. Penjelasan Alur
Urutan eksekusi middleware dan handler pada setiap request adalah sebagai berikut.
middlewareRequestID(): membaca atau men-generate request ID, menyimpannya ke contextmiddlewareAuth(): memvalidasi basic auth, membuat objekUser, menyimpannya ke contexthandleProfile(): membacaUserdan request ID dari context, menggunakannya untuk response
Di handleProfile(), nilai diambil dari context menggunakan type assertion:
user, ok := r.Context().Value(contextKeyUser).(User)
Nilai yang disimpan di context bertipe any, sehingga perlu di-assert ke tipe yang tepat. Selalu gunakan bentuk dua nilai (value, ok) agar aman jika key tidak ditemukan atau nilainya bukan tipe yang diharapkan.
Lebih jelasnya mengenai context dibahas di chapter A.65. Concurrency Pattern: Context Cancellation Pipeline
B.29.5. Testing
Jalankan server lalu coba tiga skenario berikut.
# Request tanpa auth header
curl http://localhost:9000/profile
Response: unauthorized dengan status 401.
# Request dengan basic auth
curl --user batman:secret http://localhost:9000/profile
Response: Hello, batman [admin] dengan status 200. Server juga mencetak log seperti [auto-generated-001] profile accessed by batman (admin).
# Request dengan basic auth dan custom request ID
curl --user batman:secret \
-H "X-Request-ID: req-abc-123" \
http://localhost:9000/profile
Response tetap sama, namun log di server mencetak [req-abc-123] profile accessed by batman (admin). Response header X-Request-ID: req-abc-123 juga dikembalikan ke client.