Panic nil pointer di Go Fiber yang muncul sesekali saat request melewati middleware auth berantai hampir selalu berhubungan dengan asumsi state request yang tidak selalu terpenuhi. Kasus yang paling sering: handler atau middleware logging menganggap data user di Locals atau context pasti ada, padahal pada jalur error tertentu nilai itu belum diinisialisasi, sudah gagal diparse, atau middleware dieksekusi dalam urutan yang salah.
Pada artikel ini, kita fokus pada pola bug yang nyata di backend produksi: panic sporadis, sulit direproduksi, request tertentu memicu crash, stack trace mengarah ke middleware yang tampak aman, tetapi akar masalahnya ada pada rantai auth dan context user. Tujuannya bukan hanya memperbaiki panic, tetapi juga membangun pola debugging dan pencegahan agar regresi tidak terulang.
Gejala di Produksi: Panic Sporadis, Sulit Direproduksi
Masalah ini biasanya tidak muncul pada semua request. Justru yang membuatnya mahal adalah sifatnya yang sporadis. Misalnya:
- Endpoint normal berjalan baik untuk user yang sudah login.
- Request tanpa header Authorization kadang hanya mengembalikan 401, tetapi kadang memicu panic.
- Request dengan token rusak, expired, atau format header tidak valid lebih sering memicu crash.
- Health check atau endpoint publik tidak bermasalah, tetapi route yang memakai grup middleware tertentu sering crash.
Contoh log panic yang umum terlihat:
panic: runtime error: invalid memory address or nil pointer dereference
goroutine 2145 [running]:
app/middleware.RequestLogger.func1(0xc0004a4008)
/app/middleware/logger.go:27 +0x89
github.com/gofiber/fiber/v2.(*App).next(0xc0001f2000, 0xc0004a4008)
...
app/middleware.RequireAuth.func1(0xc0004a4008)
/app/middleware/auth.go:41 +0x1c7
github.com/gofiber/fiber/v2.(*App).next(0xc0001f2000, 0xc0004a4008)
...
app/handler.Profile(0xc0004a4008)
/app/handler/profile.go:14 +0x32Dari stack trace di atas, panic muncul di logger, bukan di auth. Ini sering menyesatkan. Middleware logging terlihat sebagai sumber crash karena ia mencoba membaca field dari object user yang ternyata nil. Padahal akar masalahnya bisa berasal dari auth middleware yang tidak selalu mengisi Locals("user").
Pola Request yang Sering Memicu Bug
Ketika panic hanya terjadi pada sebagian request, perhatikan pola berikut:
- Authorization header hilang: middleware auth keluar lebih awal tanpa menyetel context user.
- Format bearer token salah: parsing gagal, tetapi middleware berikutnya tetap berjalan.
- Token valid secara struktur, tetapi user tidak ditemukan: proses lookup mengembalikan
nil, nilatau pointer kosong. - Route publik memakai middleware logging yang mengasumsikan user wajib ada.
- Urutan middleware tidak konsisten antara grup route A dan grup route B.
- Error path dari middleware tertentu melakukan
return c.Next()padahal seharusnya berhenti dengan error response.
Jika panic meningkat saat ada lonjakan request tidak valid dari klien mobile, crawler, atau traffic bot, itu petunjuk kuat bahwa bug terkait dengan jalur auth yang gagal, bukan jalur sukses.
Studi Kasus: Middleware Auth, Logging, dan User Context
Misalkan kita punya tiga komponen:
- Auth middleware untuk memverifikasi token dan mengisi user ke context request.
- Request logger middleware yang mencatat user ID, path, dan status.
- Handler yang membaca user dari context.
Versi buggy sering terlihat sederhana dan tampak masuk akal pada review cepat.
Contoh Kode Buggy
type User struct {
ID string
Email string
}
func RequireAuth(userSvc UserService) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
// niatnya membiarkan handler lain memutuskan
return c.Next()
}
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := ParseToken(token)
if err != nil {
// bug: request tetap dilanjutkan walau token gagal diparse
return c.Next()
}
user, err := userSvc.FindByID(c.Context(), claims.UserID)
if err != nil {
return fiber.ErrUnauthorized
}
// bug potensial: user bisa nil jika service tidak menemukan data
c.Locals("user", user)
return c.Next()
}
}
func RequestLogger() fiber.Handler {
return func(c *fiber.Ctx) error {
err := c.Next()
// bug: mengasumsikan user selalu ada dan bertipe *User
user := c.Locals("user").(*User)
log.Printf("path=%s status=%d user_id=%s", c.Path(), c.Response().StatusCode(), user.ID)
return err
}
}
func Profile(c *fiber.Ctx) error {
user := c.Locals("user").(*User)
return c.JSON(fiber.Map{
"id": user.ID,
})
}Ada beberapa masalah sekaligus:
RequireAuthtetap memanggilc.Next()meskipun header kosong atau token invalid.FindByIDmungkin mengembalikan pointerniltanpa error eksplisit, lalu tetap disimpan keLocals.RequestLoggerdan handler melakukan type assertion langsung tanpa memeriksa keberadaan nilai.- Desain route mungkin mencampur endpoint publik dan endpoint privat dengan middleware yang sama, tetapi asumsi state tidak sama.
Root Cause yang Paling Sering Terjadi
1. Asumsi bahwa Locals("user") selalu ada
Ini akar masalah paling umum. Dalam sistem nyata, ada banyak jalur request yang tidak menghasilkan user:
- token hilang, invalid, expired
- error parsing header
- lookup user gagal
- route memang publik
Jika middleware atau handler memakai:
user := c.Locals("user").(*User)maka panic bisa terjadi dalam dua kondisi:
c.Locals("user")mengembalikannil- nilainya ada tetapi tipenya bukan
*User
2. Urutan middleware salah
Logger yang membutuhkan user context harus tahu apakah ia berjalan:
- sebelum auth,
- sesudah auth,
- atau pada grup route yang campur publik dan privat.
Contoh urutan yang rawan:
app.Use(RequestLogger())
app.Use(RequireAuth(userSvc))Jika logger mengakses user setelah c.Next(), ia memang bisa melihat hasil auth. Tetapi bila auth punya jalur yang tidak mengisi user dan tetap meneruskan request, logger tetap panic. Jadi urutan saja tidak cukup; kontrak antar middleware juga harus jelas.
3. Error path melewati inisialisasi
Bug sering tersembunyi pada cabang error. Di jalur sukses, semua baik. Di jalur gagal, middleware melakukan salah satu dari ini:
return c.Next()padahal seharusnya mengembalikan 401/403- mengisi sebagian state, tetapi tidak lengkap
- mengembalikan error tanpa menyetel nilai default yang diasumsikan middleware lain
Karena request invalid biasanya persentasenya kecil, panic tampak acak padahal deterministik terhadap cabang kode tertentu.
4. Nilai user ada, tetapi pointer-nya nil
Ini lebih halus. Misalnya service mengembalikan (*User)(nil), nil. Type assertion ke *User bisa sukses, tetapi saat field diakses tetap panic:
u := c.Locals("user").(*User)
_ = u.ID // panic jika u == nilJadi pemeriksaan tidak cukup hanya di type assertion; pointer hasil akhirnya juga harus diperiksa.
Proses Isolasi Masalah di Produksi
1. Tambahkan recover middleware lebih awal
Pada aplikasi Fiber, pastikan panic tidak langsung mematikan request tanpa log yang cukup. Recover middleware berguna untuk menangkap stack trace dan menjaga service tetap merespons.
app.Use(recover.New())Jika ingin investigasi lebih baik, log detail request yang aman untuk dicatat: path, method, request ID, ada/tidaknya Authorization header, dan status akhir. Jangan log token mentah.
2. Korelasikan panic dengan pola request
Tambahkan field observabilitas yang membantu:
- request ID
- route/path
- apakah header Authorization ada
- hasil parsing auth: missing / malformed / invalid / ok
- apakah user context terisi
Contoh logging defensif di auth middleware:
func RequireAuth(userSvc UserService) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
log.Printf("req_id=%s auth=missing path=%s", c.Get("X-Request-ID"), c.Path())
return fiber.ErrUnauthorized
}
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := ParseToken(token)
if err != nil {
log.Printf("req_id=%s auth=invalid_token path=%s err=%v", c.Get("X-Request-ID"), c.Path(), err)
return fiber.ErrUnauthorized
}
user, err := userSvc.FindByID(c.Context(), claims.UserID)
if err != nil || user == nil {
log.Printf("req_id=%s auth=user_not_found path=%s user_id=%s", c.Get("X-Request-ID"), c.Path(), claims.UserID)
return fiber.ErrUnauthorized
}
c.Locals("user", user)
return c.Next()
}
}Dengan log seperti ini, Anda bisa melihat apakah panic hanya terjadi setelah auth=missing atau auth=invalid_token.
3. Gunakan stack trace untuk mencari titik akses pertama
Jangan berhenti pada frame paling atas. Telusuri:
- siapa yang pertama kali membaca
Locals("user") - apakah panic terjadi setelah
c.Next()atau sebelum - middleware mana yang seharusnya menginisialisasi state tersebut
Sering kali logger terlihat bersalah, tetapi handler juga punya bug serupa. Perbaiki kontrak sistemik, bukan hanya titik panic yang pertama terlihat.
4. Reproduksi lokal dengan request yang memicu cabang error
Bug seperti ini tidak akan muncul jika hanya diuji dengan token valid. Buat matriks request sederhana:
- tanpa Authorization header
Authorization: Bearertanpa token- token random
- token valid untuk user yang sudah dihapus
- route publik yang tetap melewati logger
Contoh perintah:
curl -i http://localhost:3000/profile
curl -i -H "Authorization: Bearer" http://localhost:3000/profile
curl -i -H "Authorization: Bearer invalid-token" http://localhost:3000/profile
curl -i -H "Authorization: Bearer VALID_BUT_UNKNOWN_USER" http://localhost:3000/profileJika panic hanya muncul pada salah satu pola, fokuskan debugging ke cabang auth tersebut.
5. Pakai pprof bila panic berkaitan dengan lonjakan traffic atau perilaku kompleks
pprof bukan alat utama untuk menemukan nil pointer, tetapi berguna saat panic sporadis muncul bersama gejala lain seperti goroutine menumpuk, latensi tinggi, atau timeout yang membuat jalur error lebih sering dieksekusi. Dengan pprof, Anda bisa memeriksa apakah ada blocking, retry berlebihan, atau backlog yang memperbesar kemungkinan request masuk ke error path tertentu.
Gunakan pprof sebagai alat pendukung, bukan pengganti stack trace dan logging terstruktur.
Versi Perbaikan: Defensif, Jelas, dan Konsisten
Perbaikan yang baik biasanya mencakup tiga prinsip:
- Kontrak middleware jelas: route privat wajib berhenti jika auth gagal.
- Akses context aman: jangan type assertion langsung tanpa validasi.
- Pemisahan route publik dan privat: jangan paksa middleware generik mengasumsikan user selalu ada.
Perbaiki Auth Middleware
type ctxKey string
const userLocalKey ctxKey = "user"
func RequireAuth(userSvc UserService) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return fiber.ErrUnauthorized
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return fiber.ErrUnauthorized
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
return fiber.ErrUnauthorized
}
claims, err := ParseToken(token)
if err != nil {
return fiber.ErrUnauthorized
}
user, err := userSvc.FindByID(c.Context(), claims.UserID)
if err != nil || user == nil {
return fiber.ErrUnauthorized
}
c.Locals(string(userLocalKey), user)
return c.Next()
}
}Di sini, auth middleware tidak lagi membiarkan request privat lolos tanpa user valid. Ini menghilangkan banyak state ambigu.
Buat Helper Pengambilan User yang Aman
func CurrentUser(c *fiber.Ctx) (*User, bool) {
v := c.Locals(string(userLocalKey))
if v == nil {
return nil, false
}
user, ok := v.(*User)
if !ok || user == nil {
return nil, false
}
return user, true
}Dengan helper ini, middleware dan handler tidak perlu mengulang type assertion mentah yang rawan panic.
Perbaiki Logger agar Tidak Mengasumsikan User Selalu Ada
func RequestLogger() fiber.Handler {
return func(c *fiber.Ctx) error {
err := c.Next()
userID := "anonymous"
if user, ok := CurrentUser(c); ok {
userID = user.ID
}
log.Printf("path=%s status=%d user_id=%s", c.Path(), c.Response().StatusCode(), userID)
return err
}
}Ini penting karena logger biasanya dipakai lintas route. Logger sebaiknya tahan terhadap request anonim, jalur unauthorized, atau endpoint publik.
Perbaiki Handler
func Profile(c *fiber.Ctx) error {
user, ok := CurrentUser(c)
if !ok {
return fiber.ErrUnauthorized
}
return c.JSON(fiber.Map{
"id": user.ID,
})
}Walau route sudah diproteksi auth middleware, validasi defensif di handler tetap berguna. Ini memberi perlindungan tambahan jika ada perubahan routing di masa depan.
Susun Grup Route dengan Kontrak yang Konsisten
app.Use(recover.New())
app.Use(RequestLogger())
api := app.Group("/api")
public := api.Group("/public")
private := api.Group("/private", RequireAuth(userSvc))
public.Get("/ping", Ping)
private.Get("/profile", Profile)Pola ini lebih aman daripada mencampur endpoint publik dan privat dalam satu grup dengan asumsi user context selalu tersedia.
Langkah Reproduksi Lokal yang Disarankan
Supaya bug tidak hanya dibahas secara teoritis, berikut alur reproduksi yang realistis.
1. Siapkan versi buggy
Gunakan auth middleware yang tetap memanggil c.Next() pada token invalid, lalu logger yang mengakses Locals("user") tanpa pengecekan.
2. Kirim request invalid berulang
for i in $(seq 1 20); do
curl -s -o /dev/null -H "Authorization: Bearer invalid-token" http://localhost:3000/profile
doneJika panic tidak langsung muncul, coba kombinasi request valid dan invalid. Pada beberapa sistem, bug terlihat hanya saat route tertentu, code path tertentu, atau respons error tertentu terjadi.
3. Tambahkan log di titik masuk dan keluar middleware
log.Printf("enter auth path=%s", c.Path())
log.Printf("auth user set user_id=%s", user.ID)
log.Printf("exit logger has_user=%t", c.Locals("user") != nil)Tujuannya bukan mempercantik log, tetapi membuktikan urutan eksekusi dan state request di setiap cabang.
4. Bandingkan hasil sebelum dan sesudah perbaikan
Setelah perbaikan:
- request tanpa token harus konsisten menghasilkan 401
- request invalid token tidak boleh panic
- logger tetap berjalan tanpa mengakses pointer nil
- handler privat hanya menerima user valid
Teknik Debugging yang Relevan
Recover middleware
Gunakan untuk menangkap panic dan memperoleh stack trace. Ini penting di produksi agar satu request gagal tidak merusak stabilitas service.
Structured logging
Log yang baik untuk kasus ini minimal memuat:
- request ID
- method dan path
- hasil auth
- status code
- indikator ada/tidaknya user context
Hindari mencatat data sensitif seperti token mentah atau payload JWT penuh.
pprof
Pakai bila Anda mencurigai panic meningkat saat service berada di bawah tekanan, ada timeout dependency, atau jalur fallback tertentu aktif. pprof membantu melihat kondisi runtime yang memperbesar frekuensi error path, meskipun bukan alat utama untuk nil pointer.
Testing per cabang error
Banyak panic nil pointer lolos ke produksi karena test hanya mencakup jalur sukses. Uji semua cabang auth: missing header, format salah, token invalid, user tidak ditemukan, dan user valid.
Contoh Testing Regresi yang Layak Dipertahankan
func TestProfile_UnauthorizedWithoutToken(t *testing.T) {
app := fiber.New()
app.Use(RequestLogger())
app.Get("/profile", RequireAuth(mockUserSvc), Profile)
req := httptest.NewRequest("GET", "/profile", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != fiber.StatusUnauthorized {
t.Fatalf("expected 401, got %d", resp.StatusCode)
}
}
func TestCurrentUser_ReturnsFalseWhenMissing(t *testing.T) {
app := fiber.New()
app.Get("/check", func(c *fiber.Ctx) error {
_, ok := CurrentUser(c)
if ok {
t.Fatal("expected no user")
}
return c.SendStatus(fiber.StatusOK)
})
req := httptest.NewRequest("GET", "/check", nil)
if _, err := app.Test(req); err != nil {
t.Fatal(err)
}
}Test seperti ini sederhana, tetapi efektif untuk mencegah kembalinya asumsi berbahaya soal context user.
Checklist Pencegahan untuk Code Review dan Testing
Checklist code review
- Apakah ada penggunaan
c.Locals(...).(*T)tanpa pengecekan? - Apakah auth middleware pernah melanjutkan request setelah validasi gagal?
- Apakah service bisa mengembalikan pointer nil tanpa error, dan apakah itu ditangani?
- Apakah logger atau metrics middleware mengasumsikan user selalu ada?
- Apakah route publik dan privat dipisahkan dengan jelas?
- Apakah urutan middleware konsisten di semua grup route?
- Apakah error path diuji, bukan hanya jalur sukses?
Checklist testing regresi
- Request tanpa Authorization header menghasilkan 401, bukan panic
- Header bearer kosong menghasilkan 401
- Token invalid menghasilkan 401
- Token valid dengan user tidak ditemukan menghasilkan 401
- Logger tetap aman pada semua respons error
- Handler privat tidak pernah mengakses user tanpa validasi
- Stack trace panic tidak lagi muncul pada beban request invalid berulang
Penutup
Go Fiber: debug panic nil pointer saat middleware auth berantai hampir selalu kembali ke satu prinsip: jangan mengandalkan state request yang tidak dijamin ada. Jika user context hanya tersedia pada jalur auth sukses, maka semua middleware dan handler lain harus memperlakukan nilai itu sebagai opsional kecuali kontraknya benar-benar ketat.
Perbaikan yang tahan lama bukan sekadar menambah if nil di satu titik, tetapi memastikan tiga hal: auth gagal harus berhenti dengan jelas, akses Locals harus aman, dan rantai middleware harus punya kontrak yang konsisten. Dengan logging yang tepat, recover middleware, reproduksi lokal untuk cabang error, dan test regresi yang mencakup request invalid, panic sporadis semacam ini biasanya bisa diubah menjadi bug yang deterministik dan mudah dihilangkan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!