Cookie auth hilang di Route Handler production biasanya bukan bug acak. Polanya sering sama: di lokal semuanya berjalan normal, tetapi setelah deploy user tiba-tiba sering logout, request ke endpoint internal mulai mengembalikan 401 Unauthorized, dan masalah hanya muncul di environment tertentu seperti staging atau production. Dalam banyak kasus, penyebab utamanya ada pada atribut cookie, perbedaan HTTP vs HTTPS, domain yang tidak cocok, atau cara cookie dibaca dan diteruskan di runtime yang berbeda.

Artikel ini membahas studi kasus debugging backend Next.js untuk kasus tersebut. Fokusnya bukan pada UI login, tetapi pada alur request-response di server: bagaimana cookie diset, kapan browser mengirimkannya, bagaimana Route Handler membacanya, dan kenapa perilakunya bisa berbeda antara lokal dan production.

Gejala yang Muncul di Production

Kasus ini biasanya terlihat dari kombinasi gejala berikut:

  • User berhasil login, tetapi beberapa menit kemudian terlihat seperti logout sendiri.
  • Request ke endpoint internal seperti /api/me, /api/session, atau proxy ke backend lain mulai mengembalikan 401.
  • Bug sulit direproduksi di lokal, terutama jika lokal berjalan di http://localhost.
  • Masalah hanya muncul pada domain tertentu, subdomain tertentu, atau saat request melewati CDN / reverse proxy.
  • Di browser DevTools, cookie terlihat ada, tetapi tidak selalu ikut terkirim pada request yang diharapkan.

Jika gejalanya seperti ini, jangan langsung menyimpulkan bahwa token rusak atau session store bermasalah. Sering kali token valid, tetapi cookie tidak pernah terkirim ke Route Handler yang membutuhkannya.

Studi Kasus Singkat: Login Berhasil, Endpoint Internal 401

Misalkan arsitekturnya seperti ini:

  • Aplikasi Next.js memiliki Route Handler di /api/auth/login untuk membuat session cookie.
  • Route Handler lain, misalnya /api/profile, membaca cookie tersebut untuk memverifikasi user.
  • Dalam beberapa kasus, Route Handler Next.js juga meneruskan request ke backend internal lain.

Di lokal, alurnya tampak benar:

  1. User login.
  2. Server mengirim Set-Cookie.
  3. Browser menyimpan cookie.
  4. Request berikutnya ke Route Handler membawa header Cookie.
  5. Server membaca session dan mengembalikan data user.

Namun di production, langkah ketiga atau keempat sering gagal. Cookie mungkin tidak disimpan, hanya tersimpan untuk path tertentu, tidak cocok dengan domain aktif, atau tidak ikut dikirim karena atribut keamanan browser.

Root Cause Teknis yang Paling Sering

1. Atribut Secure tidak sesuai dengan HTTP/HTTPS

Cookie dengan atribut Secure hanya akan dikirim melalui HTTPS. Ini benar dan memang diharapkan untuk production. Masalah muncul ketika:

  • Aplikasi mengira koneksi masih HTTP karena konfigurasi proxy tidak benar.
  • Environment staging memakai HTTP biasa, tetapi cookie tetap diset dengan Secure.
  • Di lokal cookie diset tanpa Secure, lalu di production berubah perilaku karena domain dan skema berubah.

Akibatnya, browser bisa menolak menyimpan cookie atau menyimpannya tetapi tidak mengirimkannya pada request tertentu.

2. Nilai SameSite tidak cocok dengan pola request

SameSite menentukan kapan cookie boleh ikut pada request lintas origin atau lintas site. Salah konfigurasi di sini bisa membuat login tampak berhasil, tetapi request berikutnya tidak membawa cookie.

  • SameSite=Lax cukup aman untuk banyak kasus same-site biasa.
  • SameSite=None dibutuhkan untuk beberapa skenario cross-site, tetapi harus disertai Secure.
  • SameSite=Strict dapat terlalu ketat dan membuat alur tertentu gagal.

Masalah ini sering muncul jika frontend, domain auth, dan API tidak benar-benar berada dalam konteks site yang sama menurut browser.

3. domain cookie salah

Cookie sangat sensitif terhadap domain. Contoh kesalahan umum:

  • Cookie diset untuk localhost lalu dibawa mentah ke production.
  • Cookie diset untuk api.example.com, tetapi request yang perlu autentikasi datang ke app.example.com.
  • Cookie diset dengan domain yang terlalu spesifik sehingga tidak tersedia pada subdomain lain yang sebenarnya dipakai.

Jika domain tidak cocok, browser tidak akan mengirim cookie ke host target, dan Route Handler akan melihat request sebagai tidak terautentikasi.

4. path cookie terlalu sempit

Jika cookie dibuat dengan path yang terlalu spesifik, misalnya hanya /api/auth, maka cookie itu tidak akan ikut ke endpoint seperti /api/profile. Ini bug yang sering luput karena login sendiri tetap berhasil.

Untuk session utama, path=/ biasanya pilihan paling aman kecuali ada alasan kuat untuk membatasi ruang lingkupnya.

5. Cookie dibaca di runtime yang berbeda dari asumsi kode

Di Next.js, perilaku Route Handler bisa dipengaruhi oleh lingkungan eksekusi. Jika sebagian endpoint berjalan dalam runtime yang berbeda, atau request diproksikan ulang tanpa meneruskan header yang diperlukan, hasilnya bisa membingungkan:

  • Cookie ada di request awal, tetapi hilang ketika request diteruskan ke service internal.
  • Kode membaca cookie dari helper tertentu, tetapi request sebenarnya datang dari jalur yang berbeda.
  • Header Cookie tidak diteruskan saat melakukan fetch server-to-server.

Ini bukan masalah browser lagi, melainkan masalah alur backend.

6. Request internal dari server tidak otomatis membawa cookie user

Ini salah satu penyebab paling umum. Misalnya Route Handler menerima request user yang sudah punya cookie, lalu di dalam handler dilakukan fetch ke endpoint internal lain. Banyak developer mengira cookie user otomatis ikut. Padahal pada request server-to-server, Anda sering harus meneruskan header Cookie secara eksplisit.

Jika tidak diteruskan, endpoint internal kedua akan menganggap request anonim dan mengembalikan 401.

Langkah Investigasi yang Terstruktur

Saat debugging, jangan mulai dari asumsi. Verifikasi rantai berikut secara sistematis:

  1. Apakah response login benar-benar mengirim Set-Cookie yang sesuai?
  2. Apakah browser benar-benar menyimpan cookie itu?
  3. Apakah request berikutnya mengirim header Cookie?
  4. Apakah Route Handler menerima dan membaca cookie yang sama?
  5. Jika handler memanggil service lain, apakah cookie diteruskan?

Periksa response login

Gunakan DevTools browser tab Network atau curl untuk melihat header response dari endpoint login.

curl -i -X POST https://app.example.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"secret"}'

Hal yang perlu dicek:

  • Apakah ada header Set-Cookie?
  • Apakah nama cookie sesuai harapan?
  • Apakah atribut HttpOnly, Secure, SameSite, Domain, dan Path benar?
  • Apakah expiry atau Max-Age masuk akal?

Periksa apakah browser menyimpan cookie

Di DevTools, lihat tab Application/Storage pada domain yang aktif. Jika response mengirim Set-Cookie tetapi cookie tidak muncul, browser kemungkinan menolaknya. Penyebabnya biasanya kombinasi atribut yang tidak valid, misalnya SameSite=None tanpa Secure, atau domain yang tidak valid untuk host aktif.

Periksa request berikutnya

Lalu lihat request ke endpoint yang mengembalikan 401. Pastikan header Cookie memang terkirim. Ini langkah penting karena banyak kasus berhenti di sini: cookie tersimpan, tetapi tidak ikut pada request tertentu.

Jika cookie tidak terkirim, fokus pada domain, path, SameSite, dan konteks request.

Tambahkan logging terarah di Route Handler

Untuk sementara, tambahkan logging minimal di sisi server. Jangan log seluruh token rahasia ke production log. Cukup log metadata yang aman, misalnya nama cookie yang ada, host, path, dan apakah header Cookie hadir.

import { cookies, headers } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const cookieStore = await cookies();
  const headerStore = await headers();

  const session = cookieStore.get('session');
  const host = headerStore.get('host');
  const cookieHeaderPresent = !!headerStore.get('cookie');

  console.log('[auth-debug]', {
    host,
    cookieHeaderPresent,
    hasSessionCookie: !!session,
    cookieNames: cookieStore.getAll().map((c) => c.name),
  });

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  return NextResponse.json({ ok: true });
}

Dengan log seperti ini, Anda bisa membedakan apakah masalahnya ada di browser, di edge/proxy, atau di kode pembacaan cookie.

Verifikasi request yang diteruskan antar-service

Jika handler memanggil endpoint internal lain, log header request keluar. Sekali lagi, jangan log isi token penuh. Verifikasi apakah header Cookie atau header auth pengganti benar-benar diteruskan.

import { headers } from 'next/headers';

export async function GET() {
  const incomingHeaders = await headers();
  const cookieHeader = incomingHeaders.get('cookie') || '';

  const res = await fetch('https://internal.example.com/session/validate', {
    method: 'GET',
    headers: {
      Cookie: cookieHeader,
    },
    cache: 'no-store',
  });

  // ...
}

Tanpa penerusan ini, endpoint internal tidak punya konteks autentikasi user.

Contoh Konfigurasi yang Salah dan yang Benar

Contoh salah: domain dan path terlalu sempit

response.cookies.set('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  domain: 'api.example.com',
  path: '/api/auth',
});

Masalah dari konfigurasi ini:

  • Cookie hanya berlaku untuk api.example.com, padahal aplikasi mungkin berjalan di app.example.com.
  • Cookie hanya berlaku untuk path /api/auth, sehingga request ke endpoint lain tidak akan membawanya.

Contoh lebih tepat untuk session aplikasi utama

response.cookies.set('session', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  path: '/',
  // domain diisi hanya jika benar-benar perlu lintas subdomain
  // domain: '.example.com',
  maxAge: 60 * 60 * 24 * 7,
});

Kenapa ini lebih aman:

  • path=/ memastikan cookie tersedia untuk seluruh aplikasi.
  • secure hanya aktif di production HTTPS.
  • sameSite=lax cocok untuk banyak skenario same-site standar.
  • domain tidak dipaksakan jika tidak perlu, sehingga mengurangi risiko salah scope.

Catatan: Jangan menambahkan domain hanya karena terlihat lebih “lengkap”. Jika aplikasi tidak butuh berbagi cookie antar-subdomain, sering kali lebih aman membiarkannya default ke host saat ini.

Contoh salah: SameSite=None tanpa Secure

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

Konfigurasi ini bermasalah karena browser modern mensyaratkan Secure ketika memakai SameSite=None.

Contoh benar untuk skenario yang memang butuh cross-site

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

Gunakan ini hanya jika Anda benar-benar punya kebutuhan cross-site. Jika tidak, Lax biasanya lebih sederhana dan aman.

Perbaikan Kode pada Route Handler

Berikut contoh Route Handler login dan endpoint proteksi yang lebih konsisten.

Login: set cookie dengan atribut yang sesuai environment

import { NextResponse } from 'next/server';

export async function POST(req) {
  const { email, password } = await req.json();

  // Ganti dengan verifikasi kredensial yang sebenarnya
  const isValidUser = email && password;
  if (!isValidUser) {
    return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
  }

  const token = 'signed-session-token';
  const response = NextResponse.json({ ok: true });

  response.cookies.set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24,
  });

  return response;
}

Protected route: baca cookie dari request aktif

import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const cookieStore = await cookies();
  const session = cookieStore.get('session')?.value;

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Validasi token/session sesuai backend Anda
  return NextResponse.json({ user: { id: '123', email: 'user@example.com' } });
}

Jika perlu meneruskan autentikasi ke endpoint internal

import { headers } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const headerStore = await headers();
  const cookieHeader = headerStore.get('cookie');

  const upstream = await fetch('https://internal.example.com/api/me', {
    headers: cookieHeader ? { Cookie: cookieHeader } : {},
    cache: 'no-store',
  });

  if (upstream.status === 401) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const data = await upstream.json();
  return NextResponse.json(data);
}

Inti perbaikannya ada dua:

  • Pastikan cookie diset dengan scope yang benar.
  • Pastikan cookie dibaca dan diteruskan secara eksplisit jika alurnya melibatkan request server-to-server.

Checklist Debugging Deployment

Sebelum menyimpulkan bug ada di Next.js, cek poin-poin ini:

  • HTTPS aktif di production dan sesuai dengan asumsi atribut Secure.
  • Host dan subdomain konsisten antara URL login, URL aplikasi, dan endpoint yang butuh session.
  • Cookie path tidak terlalu sempit.
  • SameSite sesuai dengan konteks request nyata.
  • Proxy / load balancer / CDN tidak memodifikasi header penting secara tak terduga.
  • Redirect antar-domain tidak mengubah origin sehingga cookie tak lagi cocok.
  • Server-side fetch yang bergantung pada session meneruskan header auth yang diperlukan.
  • Environment variable untuk base URL tidak menyebabkan request internal pergi ke host yang berbeda dari cookie scope.

Kesalahan Umum yang Sering Menyesatkan

Menganggap lokal mewakili production

localhost bukan simulasi sempurna untuk domain production. Perbedaan skema, subdomain, proxy, dan kebijakan browser membuat bug cookie sering tersembunyi di lokal.

Mengandalkan pembacaan cookie tanpa melihat header mentah

Helper framework memudahkan akses cookie, tetapi saat debugging Anda tetap perlu melihat header request/response secara langsung. Dari sana biasanya masalah cepat terlihat.

Menyet domain cookie terlalu cepat

Banyak tim langsung menambahkan domain=.example.com tanpa benar-benar perlu. Ini bisa membantu jika Anda memang berbagi session antar-subdomain, tetapi juga bisa menambah kompleksitas dan risiko salah konfigurasi.

Tidak membedakan request browser dan request server

Browser punya mekanisme otomatis untuk mengirim cookie. Request dari server tidak selalu mengikuti aturan itu secara otomatis. Jika Route Handler memanggil endpoint lain, Anda harus memperlakukan itu sebagai request baru dengan header baru.

Cara Mencegah Regresi

Buat uji integrasi untuk skenario login dan endpoint proteksi

Minimal pastikan skenario berikut diuji pada environment yang menyerupai production:

  • Login mengembalikan Set-Cookie dengan atribut yang benar.
  • Request kedua ke endpoint proteksi berhasil dengan cookie tersebut.
  • Route Handler yang memanggil service internal tetap mempertahankan konteks auth.

Log metadata auth yang aman

Tambahkan logging yang tidak membocorkan rahasia, misalnya:

  • apakah header Cookie hadir,
  • nama cookie yang terbaca,
  • host dan path request,
  • status response dari upstream auth service.

Ini sangat membantu saat bug hanya muncul di production.

Dokumentasikan kebijakan cookie per environment

Tentukan dengan jelas:

  • kapan secure harus aktif,
  • apakah aplikasi membutuhkan lintas subdomain,
  • nilai SameSite yang dipakai dan alasannya,
  • base URL mana yang boleh dipakai untuk internal fetch.

Tanpa dokumentasi seperti ini, bug lama mudah kembali saat ada perubahan domain, proxy, atau strategi deploy.

Kesimpulan

Kasus Debugging Next.js: Cookie Auth Hilang di Route Handler Production hampir selalu bisa dipecahkan jika Anda memeriksa alur autentikasi dari level HTTP, bukan hanya dari kode bisnis. Fokuskan investigasi pada empat hal: bagaimana cookie diset, apakah browser menyimpannya, apakah cookie ikut pada request yang gagal, dan apakah Route Handler meneruskan konteks auth saat memanggil service lain.

Dalam studi kasus seperti ini, akar masalah paling sering ada pada kombinasi Secure, SameSite, domain, path, perbedaan HTTP/HTTPS, atau asumsi keliru bahwa request internal otomatis membawa cookie user. Begitu rantai ini diverifikasi satu per satu, bug yang sebelumnya terasa acak biasanya berubah menjadi masalah konfigurasi yang sangat konkret dan bisa diperbaiki permanen.