Masalah cookie session hilang di Middleware dan API Route pada Next.js biasanya terlihat membingungkan: user merasa masih login, cookie tampak ada di browser, tetapi server sesekali menganggap sesi tidak valid. Gejalanya sering acak, sulit direproduksi di lokal, dan baru muncul di staging, preview deployment, atau production di balik proxy/CDN.

Jika Anda mengalami kondisi seperti ini, akar masalahnya hampir selalu bukan “Next.js lupa membaca cookie”, melainkan kombinasi detail HTTP yang halus: domain dan path cookie tidak cocok, atribut SameSite/Secure membuat cookie tidak ikut terkirim, header Set-Cookie tertimpa, atau ada perbedaan perilaku saat membaca cookie antara Middleware dan API Route/Route Handler. Artikel ini membahas studi kasus debugging yang realistis, termasuk cara investigasi, reproduksi bug, perbaikan, dan checklist pencegahannya.

Gejala yang Biasanya Muncul

Pada kasus nyata, bug ini jarang muncul sebagai error yang jelas. Yang terlihat justru gejala-gejala berikut:

  • User acak logout setelah navigasi tertentu atau setelah request ke endpoint internal.
  • Di browser DevTools, cookie sesi terlihat masih ada.
  • Di Middleware, cookie kadang terbaca; di API Route atau Route Handler, cookie yang sama kadang kosong.
  • Bug hanya muncul di environment tertentu, misalnya staging di subdomain, preview deployment, atau production di belakang load balancer.
  • Login berhasil, tetapi request berikutnya dianggap tidak punya sesi.

Polanya penting: jika browser menampilkan cookie tetapi server tidak selalu menerimanya, masalahnya sering terjadi pada pengiriman cookie dari browser ke server atau penulisan ulang cookie oleh server/proxy, bukan pada penyimpanan di browser saja.

Memahami Alur Request: Kenapa Middleware dan API Route Bisa Berbeda?

Di Next.js, request dapat melewati Middleware sebelum masuk ke halaman, API Route, atau Route Handler. Secara konsep, cookie berasal dari header Cookie pada request masuk. Namun dalam praktiknya, ada beberapa sumber perbedaan:

  • Runtime berbeda: Middleware berjalan di runtime yang berbeda dari Node.js API Route pada beberapa deployment, sehingga cara akses object request/response dan perilaku utilitas cookie bisa berbeda.
  • Mutasi response: Middleware dapat menambahkan atau mengubah header, termasuk Set-Cookie. Jika tidak hati-hati, header yang sudah ada bisa tertimpa.
  • Scope cookie: Cookie dengan Path atau Domain tertentu mungkin terkirim ke satu endpoint tetapi tidak ke endpoint lain.
  • Cross-site behavior: Redirect lintas origin, subdomain, atau callback auth dapat memicu aturan SameSite yang membuat cookie tidak ikut terkirim.

Artinya, jika cookie terlihat “ada”, belum tentu cookie itu eligible untuk request yang sedang diuji.

Studi Kasus: Session Hilang Setelah Lewat Middleware

Misalkan aplikasi punya alur seperti ini:

  1. User login di app.example.com.
  2. Server mengirim Set-Cookie: session=....
  3. Setiap request ke halaman privat melewati Middleware untuk validasi ringan.
  4. Dari halaman tersebut, frontend memanggil /api/me atau Route Handler internal untuk memuat data user.

Gejalanya:

  • Middleware kadang mendeteksi session.
  • Request ke /api/me kadang tidak membawa cookie.
  • Masalah hanya muncul di staging, misalnya staging.example.com atau URL preview.

Di lokal, semuanya tampak normal karena origin sederhana, biasanya tanpa proxy, tanpa HTTPS termination yang rumit, dan tanpa perbedaan subdomain.

Langkah Investigasi yang Benar

1. Pastikan cookie benar-benar ikut dalam request

Jangan berhenti pada kesimpulan “cookie ada di browser”. Yang perlu dibuktikan adalah apakah browser mengirim header Cookie pada request yang gagal.

Periksa di DevTools browser:

  • Tab Application/Storage: lihat nilai cookie, domain, path, expiry, Secure, SameSite, HttpOnly.
  • Tab Network: buka request yang gagal, cek request headers, apakah Cookie benar-benar terkirim.
  • Bandingkan request yang berhasil dan gagal.

Jika cookie ada di storage tetapi tidak ada di request header, masalahnya hampir pasti pada scope atau policy cookie.

2. Log cookie mentah di Middleware dan API Route

Jangan langsung log hasil parsing library auth. Log header mentah terlebih dahulu agar tahu masalahnya ada pada pengiriman atau parsing.

export function middleware(req) {
  console.log('[middleware] host=', req.headers.get('host'))
  console.log('[middleware] url=', req.nextUrl.pathname)
  console.log('[middleware] cookie header=', req.headers.get('cookie'))
  console.log('[middleware] session cookie=', req.cookies.get('session')?.value)

  return NextResponse.next()
}

Lalu di API Route atau Route Handler:

export async function GET(request) {
  console.log('[route] host=', request.headers.get('host'))
  console.log('[route] url=', new URL(request.url).pathname)
  console.log('[route] cookie header=', request.headers.get('cookie'))

  // Cara akses cookie tergantung pola handler yang digunakan,
  // tetapi log header mentah selalu berguna.
  return new Response(JSON.stringify({ ok: true }), {
    headers: { 'content-type': 'application/json' }
  })
}

Yang dicari:

  • Apakah header Cookie hilang total?
  • Apakah nama cookie benar tetapi nilainya berubah?
  • Apakah ada lebih dari satu cookie dengan nama mirip?
  • Apakah host/path request berbeda dari yang diasumsikan?

3. Log semua Set-Cookie saat login dan refresh session

Banyak bug terjadi bukan saat membaca cookie, melainkan saat menulis ulang cookie. Periksa response login, refresh token, callback auth, atau Middleware yang ikut menyetel cookie.

Yang perlu dilihat:

  • Apakah ada lebih dari satu header Set-Cookie untuk nama yang sama?
  • Apakah cookie session ditulis dua kali dengan Path berbeda?
  • Apakah response lain menimpa cookie dengan nilai kosong atau expiry lampau?
  • Apakah ada layer proxy/serverless platform yang memodifikasi header?

4. Bandingkan environment lokal vs staging/production

Kasus “hanya muncul di environment tertentu” biasanya sangat informatif. Buat tabel sederhana:

  • Origin aplikasi: localhost, subdomain staging, domain production.
  • HTTP vs HTTPS.
  • Apakah ada reverse proxy, ingress, CDN, atau auth gateway.
  • Apakah login callback datang dari domain berbeda.
  • Apakah API dipanggil lewat relative path atau absolute URL ke origin lain.

Sering kali bug baru terlihat ketika staging memakai subdomain yang berbeda dari domain cookie yang disetel saat login.

Root Cause yang Paling Masuk Akal

1. Domain dan Path cookie tidak cocok

Ini penyebab paling umum. Misalnya cookie dibuat hanya untuk /app, tetapi API ada di /api, maka browser tidak akan mengirim cookie itu ke endpoint API.

Contoh konfigurasi salah:

Set-Cookie: session=abc123; Path=/app; HttpOnly; Secure; SameSite=Lax

Jika frontend memanggil /api/me, cookie di atas tidak akan terkirim karena Path=/app tidak mencakup /api.

Konfigurasi yang lebih aman untuk sesi aplikasi umum:

Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax

Masalah serupa juga berlaku untuk Domain:

  • Cookie untuk app.example.com tidak otomatis berlaku ke api.example.com.
  • Jika aplikasi tersebar di beberapa subdomain, konfigurasi domain harus konsisten dengan arsitekturnya.

Jika Anda tidak memang butuh berbagi cookie lintas subdomain, biasanya lebih aman membiarkan cookie bersifat host-only daripada mengatur Domain terlalu luas.

2. Atribut SameSite dan Secure tidak cocok dengan alur login

Bug sering muncul setelah integrasi OAuth, redirect dari penyedia auth, iframe, atau request lintas origin. Cookie mungkin berhasil dibuat, tetapi tidak ikut pada request berikutnya karena aturan SameSite.

  • SameSite=Lax sering cukup untuk navigasi biasa dalam situs yang sama, tetapi bisa bermasalah pada beberapa alur cross-site.
  • SameSite=None biasanya memerlukan Secure, sehingga tanpa HTTPS cookie bisa ditolak browser.
  • Di lokal yang masih HTTP, perilaku bisa berbeda dari staging/production yang HTTPS.

Contoh konfigurasi bermasalah di environment campuran:

Set-Cookie: session=abc123; Path=/; HttpOnly; SameSite=None

Di banyak browser, SameSite=None tanpa Secure akan ditolak.

Contoh yang benar jika memang butuh cross-site cookie:

Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=None

Namun trade-off-nya jelas: Anda harus memastikan semua alur berjalan di HTTPS, dan memahami implikasi keamanan cross-site.

3. Header Set-Cookie tertimpa atau tidak digabung dengan benar

Masalah lain yang sering luput adalah response menyetel beberapa cookie, tetapi implementasi hanya menyimpan header terakhir. Misalnya login menyetel session dan csrf, lalu Middleware atau handler lain menulis ulang header tanpa append yang benar.

Contoh anti-pattern secara umum:

const response = NextResponse.next()
response.headers.set('Set-Cookie', 'session=abc123; Path=/; HttpOnly')
response.headers.set('Set-Cookie', 'csrf=token123; Path=/')

Pemanggilan kedua dapat menggantikan nilai sebelumnya, tergantung cara header dikelola di lapisan tersebut. Akibatnya cookie yang Anda kira sudah terkirim ternyata hilang.

Pendekatan yang lebih aman adalah menggunakan API cookie/response yang memang dirancang untuk menambah cookie satu per satu, lalu memverifikasi hasil akhir di response header aktual.

4. Membaca cookie berbeda antara Middleware dan handler

Walaupun sumber datanya sama-sama dari request, implementasi pembacaan cookie bisa berbeda jika Anda mencampur beberapa pola:

  • Mengakses cookie dari helper framework di satu tempat, tetapi membaca header mentah di tempat lain.
  • Mengandalkan object request yang sudah di-wrap oleh library auth, lalu membandingkannya dengan request asli.
  • Mengira cookie yang baru disetel di Middleware otomatis sudah tersedia sebagai request cookie untuk handler berikutnya dalam alur yang sama.

Poin terakhir penting: cookie yang disetel pada response Middleware bukan berarti langsung menjadi bagian dari request masuk yang sama. Cookie baru akan tersedia pada request berikutnya dari browser. Jika arsitektur Anda bergantung pada “Middleware menulis cookie, lalu API Route yang sama membacanya dalam request yang sama”, hasilnya bisa tampak tidak konsisten.

5. Absolute URL memindahkan request ke origin lain

Frontend kadang memanggil API internal dengan absolute URL, misalnya berdasarkan environment variable yang berbeda di staging. Secara tidak sadar request berpindah ke subdomain atau origin lain, sehingga cookie tidak ikut.

Contoh bermasalah:

const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL
await fetch(`${apiBase}/api/me`, {
  credentials: 'include'
})

Jika apiBase mengarah ke origin berbeda dari halaman saat ini, aturan cookie menjadi berbeda. Sementara di lokal, nilai variable itu mungkin kebetulan sama origin sehingga bug tidak muncul.

Jika API memang internal ke aplikasi yang sama, relative path sering lebih aman:

await fetch('/api/me', {
  credentials: 'same-origin'
})

Cara Mereproduksi Bug Secara Terkontrol

Supaya debugging tidak spekulatif, buat reproduksi minimal. Tujuannya bukan meniru seluruh aplikasi, tetapi membuktikan perilaku cookie.

Skenario reproduksi 1: Path salah

  1. Buat endpoint login yang menyetel cookie dengan Path=/app.
  2. Akses halaman di /app/dashboard dan pastikan Middleware bisa melihat cookie.
  3. Dari halaman itu, panggil /api/me.
  4. Lihat bahwa request ke /api/me tidak membawa cookie.

Skenario reproduksi 2: Domain berbeda

  1. Login di satu subdomain.
  2. Panggil API di subdomain lain.
  3. Bandingkan request header yang terkirim.

Skenario reproduksi 3: Set-Cookie tertimpa

  1. Saat login, kirim dua cookie.
  2. Tambahkan Middleware yang juga memodifikasi Set-Cookie.
  3. Periksa response akhir, apakah semua cookie tetap ada atau hanya salah satu.

Dengan reproduksi seperti ini, Anda bisa mengisolasi apakah masalah ada di browser policy, konfigurasi cookie, atau cara aplikasi membangun response.

Contoh Konfigurasi Salah vs Benar

Kasus 1: Path terlalu sempit

Salah:

response.cookies.set('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/app'
})

Benar jika session dipakai oleh seluruh aplikasi termasuk API:

response.cookies.set('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/'
})

Kasus 2: Butuh cross-site tetapi lupa Secure

Salah:

response.cookies.set('session', token, {
  httpOnly: true,
  sameSite: 'none',
  path: '/'
})

Benar untuk alur cross-site di HTTPS:

response.cookies.set('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'none',
  path: '/'
})

Kasus 3: Mengira cookie hasil Middleware tersedia langsung di handler yang sama

Salah secara asumsi desain:

  • Middleware memeriksa token refresh.
  • Jika expired, Middleware menyetel cookie session baru.
  • API Route yang dipanggil dalam request itu juga diharapkan membaca session baru.

Pendekatan yang lebih benar:

  • Middleware boleh menandai bahwa refresh diperlukan atau melakukan redirect ke alur refresh.
  • Jika cookie baru sudah dikirim lewat response, anggap cookie itu tersedia pada request berikutnya dari browser, bukan request yang sama.

Perbaikan yang Umumnya Efektif

1. Standarkan definisi cookie session

Jangan menyetel cookie session di banyak tempat dengan konfigurasi berbeda. Buat satu utilitas yang dipakai konsisten oleh login, refresh token, logout, dan handler terkait.

export function setSessionCookie(response, token) {
  response.cookies.set('session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/'
  })
}

Jika environment tertentu perlu penyesuaian, ubah secara eksplisit dan terdokumentasi, bukan tersebar di banyak file.

2. Gunakan relative URL untuk API internal bila memungkinkan

Ini membantu memastikan browser memperlakukan request sebagai same-origin. Jika harus memakai absolute URL, audit origin-nya dengan ketat.

3. Hindari menulis Set-Cookie secara manual jika framework menyediakan API cookie

Header Set-Cookie punya format yang mudah salah, terutama jika ada lebih dari satu cookie. API resmi framework biasanya lebih aman untuk kasus umum.

4. Pisahkan tanggung jawab Middleware dan refresh session

Middleware sebaiknya ringan: validasi cepat, redirect, rewrite, atau penolakan akses. Jika logika refresh token kompleks dan bergantung pada perubahan sesi, sering lebih mudah dikelola di endpoint khusus daripada menyandarkannya pada asumsi bahwa mutasi cookie Middleware langsung terbaca downstream.

5. Perjelas strategi domain/subdomain

Jika aplikasi memakai beberapa subdomain, putuskan dari awal:

  • Apakah cookie hanya untuk satu host tertentu?
  • Apakah perlu dibagi lintas subdomain?
  • Apakah API internal berjalan di origin yang sama atau berbeda?

Masalah cookie hampir selalu lebih mudah jika UI dan API berada di origin yang konsisten.

Verifikasi Pasca-Fix

Setelah perbaikan diterapkan, verifikasi jangan hanya berdasarkan “terasa sudah normal”. Lakukan pengecekan yang terukur:

  1. Login flow: pastikan response login mengirim Set-Cookie dengan atribut yang benar.
  2. Middleware: pastikan cookie terbaca pada request ke halaman privat.
  3. API Route/Route Handler: pastikan header Cookie tetap ada saat halaman memanggil endpoint internal.
  4. Subdomain/preview/staging: uji pada environment yang sebelumnya bermasalah, bukan hanya lokal.
  5. Refresh dan logout: cek apakah cookie lama benar-benar terganti atau terhapus.
  6. Redirect auth: jika ada OAuth/callback, uji dari awal sampai request API pertama setelah login.

Bagus juga jika Anda menyimpan contoh request/response sebelum dan sesudah fix untuk membuktikan perubahan perilaku, bukan sekadar perubahan kode.

Log dan Sinyal yang Wajib Dicek Saat Incident

  • Nilai host, x-forwarded-host, dan x-forwarded-proto bila ada proxy.
  • Request URL lengkap dan path endpoint.
  • Header cookie mentah di Middleware dan di handler tujuan.
  • Semua header set-cookie pada response login/refresh/logout.
  • Apakah request berasal dari navigasi browser, fetch same-origin, atau cross-origin.
  • Apakah bug hanya muncul pada browser tertentu atau mode private/incognito tertentu.

Jika platform deployment Anda punya edge logs atau proxy logs, korelasikan timestamp request yang gagal dengan response yang terakhir menulis cookie.

Checklist Pencegahan

  • Gunakan satu helper untuk set/clear cookie session agar atribut konsisten.
  • Tetapkan Path=/ jika session dipakai lintas halaman dan API internal.
  • Jangan set SameSite=None tanpa Secure.
  • Audit domain cookie saat memakai subdomain, preview deployment, atau custom domain.
  • Hindari absolute URL untuk API internal jika tidak diperlukan.
  • Jangan mengasumsikan cookie yang baru disetel di response Middleware langsung tersedia pada request yang sama.
  • Periksa apakah ada beberapa lapisan yang menulis Set-Cookie: auth library, Middleware, API Route, reverse proxy.
  • Tambahkan logging terstruktur untuk header cookie pada endpoint auth penting.
  • Uji login dan sesi di staging dengan topologi domain yang mirip production.

Penutup

Debug cookie session hilang di Middleware dan API Route pada Next.js jarang selesai hanya dengan membaca helper cookie. Kuncinya adalah melihat request/response HTTP apa adanya: cookie disetel di mana, dengan atribut apa, untuk domain/path apa, dan apakah benar-benar ikut pada request yang gagal.

Dalam banyak kasus, perbaikannya sederhana setelah akar masalah ditemukan: samakan Path, perbaiki SameSite/Secure, hentikan penimpaan Set-Cookie, atau rapikan origin request internal. Yang sulit justru disiplin investigasinya. Mulailah dari header mentah, bandingkan environment, lalu sederhanakan alurnya sampai perilaku cookie bisa dijelaskan secara deterministik.