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 +0x32

Dari 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, nil atau 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:

  1. RequireAuth tetap memanggil c.Next() meskipun header kosong atau token invalid.
  2. FindByID mungkin mengembalikan pointer nil tanpa error eksplisit, lalu tetap disimpan ke Locals.
  3. RequestLogger dan handler melakukan type assertion langsung tanpa memeriksa keberadaan nilai.
  4. 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") mengembalikan nil
  • 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 == nil

Jadi 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: Bearer tanpa 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/profile

Jika 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:

  1. Kontrak middleware jelas: route privat wajib berhenti jika auth gagal.
  2. Akses context aman: jangan type assertion langsung tanpa validasi.
  3. 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
 done

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