Pada aplikasi Next.js modern, bug yang paling membingungkan sering kali bukan berasal dari komponen UI, melainkan dari alur request: middleware mengubah header, cookie dibaca di lokasi yang berbeda, rewrite memindahkan tujuan request secara diam-diam, atau redirect membuat pengguna terjebak dalam loop. Ketika bug seperti ini muncul di produksi, gejalanya sering tampak sederhana, misalnya halaman selalu kembali ke login, locale salah, atau API tertentu seolah tidak pernah terpanggil. Padahal akar masalahnya biasanya ada pada urutan eksekusi dan asumsi yang keliru tentang apa yang bisa dibaca atau dimodifikasi di setiap tahap.

Artikel ini membahas cara memahami dan men-debug request flow di Next.js 16, khususnya ketika middleware, header, cookie, rewrite, dan redirect terlibat. Fokusnya praktis: memahami urutan eksekusi, mengenali kasus umum, serta menambahkan jejak observabilitas sederhana agar perilaku request lebih mudah dilacak saat menangani bug produksi.

Memahami urutan eksekusi request secara ringkas

Debugging akan jauh lebih mudah jika kita memahami model mentalnya lebih dahulu. Saat browser mengirim request ke aplikasi Next.js, ada beberapa lapisan yang dapat ikut memengaruhi hasil akhir. Detail implementasi bisa berubah antar versi atau konfigurasi deployment, tetapi secara umum urutannya dapat dipahami seperti ini:

  1. Request masuk dengan URL, method, header, cookie, dan query string.
  2. Middleware dapat membaca request dan memutuskan apakah request diteruskan, di-rewrite, atau di-redirect.
  3. Routing menentukan route mana yang akan menangani request setelah perubahan dari middleware diterapkan.
  4. Handler tujuan berjalan, misalnya halaman App Router, Route Handler, atau endpoint API.
  5. Response keluar dengan status code, header, dan kemungkinan Set-Cookie.

Ada beberapa konsekuensi penting dari urutan ini:

  • Jika middleware melakukan redirect, route tujuan awal tidak akan dieksekusi.
  • Jika middleware melakukan rewrite, URL yang diproses server dapat berbeda dari URL yang dilihat pengguna di browser.
  • Jika header atau cookie dimodifikasi di response, perubahan itu belum tentu bisa langsung dibaca oleh kode lain yang sudah telanjur berjalan pada request yang sama.
  • Bug sering terjadi karena developer mengira perubahan yang dilakukan pada satu tahap otomatis terlihat pada tahap lain tanpa memperhatikan arah aliran data.

Catatan penting: Middleware adalah tempat yang sangat kuat untuk melakukan kontrol akses, locale negotiation, atau eksperimen berbasis header. Namun karena ia berada di awal request flow, kesalahan kecil dapat berdampak ke seluruh aplikasi.

Perbedaan penting antara rewrite dan redirect

Sebelum masuk ke teknik debugging, pahami dulu dua operasi yang paling sering tertukar:

Redirect

Redirect mengirim respons 3xx ke klien dan meminta browser melakukan request baru ke lokasi lain. Ini berarti ada putaran request tambahan. Browser akan melihat URL baru, histori navigasi bisa berubah, dan loop mudah terjadi bila kondisinya salah.

Rewrite

Rewrite memetakan request ke tujuan internal lain tanpa mengubah URL yang terlihat di browser. Dari sudut pandang pengguna, URL tetap sama, tetapi handler yang dieksekusi bisa berbeda. Ini berguna untuk locale, proxy internal, atau pemetaan route lama ke route baru tanpa memaksa browser pindah URL.

Secara praktis:

  • Pilih redirect jika Anda memang ingin klien berpindah ke URL lain, misalnya dari /login ke /dashboard setelah autentikasi.
  • Pilih rewrite jika Anda ingin menyajikan konten dari path lain tanpa mengubah URL publik, misalnya memetakan / ke /id berdasarkan locale.

Kesalahan umum adalah memakai redirect padahal rewrite lebih tepat. Akibatnya, muncul request tambahan, caching menjadi lebih sulit dipahami, dan kemungkinan loop meningkat.

Contoh middleware: locale dan proteksi area tertentu

Berikut contoh middleware yang cukup realistis: ia menentukan locale default dan memproteksi area /dashboard. Tujuannya bukan membuat logika paling kompleks, melainkan memberi pola yang mudah di-debug.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const PUBLIC_FILE = /\.(.*)$/
const SUPPORTED_LOCALES = ['id', 'en']
const DEFAULT_LOCALE = 'id'

export function middleware(request: NextRequest) {
  const { pathname, search } = request.nextUrl
  const requestId = crypto.randomUUID()

  // Hindari memproses asset dan route internal
  if (
    pathname.startsWith('/_next') ||
    pathname.startsWith('/api') ||
    PUBLIC_FILE.test(pathname)
  ) {
    const response = NextResponse.next()
    response.headers.set('x-request-id', requestId)
    response.headers.set('x-middleware-skip', '1')
    return response
  }

  const localeCookie = request.cookies.get('locale')?.value
  const hasLocalePrefix = SUPPORTED_LOCALES.some(
    (locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)
  )

  // Locale negotiation sederhana
  if (!hasLocalePrefix) {
    const locale = SUPPORTED_LOCALES.includes(localeCookie || '')
      ? localeCookie
      : DEFAULT_LOCALE

    const url = request.nextUrl.clone()
    url.pathname = `/${locale}${pathname}`

    const response = NextResponse.rewrite(url)
    response.headers.set('x-request-id', requestId)
    response.headers.set('x-debug-locale', locale)
    response.headers.set('x-debug-action', 'rewrite-locale')
    return response
  }

  // Proteksi area dashboard
  const token = request.cookies.get('session')?.value
  const isDashboard = pathname.startsWith('/id/dashboard') || pathname.startsWith('/en/dashboard')
  const isLogin = pathname === '/id/login' || pathname === '/en/login'

  if (isDashboard && !token) {
    const locale = pathname.split('/')[1]
    const loginUrl = request.nextUrl.clone()
    loginUrl.pathname = `/${locale}/login`
    loginUrl.searchParams.set('from', pathname + search)

    const response = NextResponse.redirect(loginUrl)
    response.headers.set('x-request-id', requestId)
    response.headers.set('x-debug-action', 'redirect-login')
    return response
  }

  if (isLogin && token) {
    const locale = pathname.split('/')[1]
    const dashboardUrl = request.nextUrl.clone()
    dashboardUrl.pathname = `/${locale}/dashboard`
    dashboardUrl.search = ''

    const response = NextResponse.redirect(dashboardUrl)
    response.headers.set('x-request-id', requestId)
    response.headers.set('x-debug-action', 'redirect-dashboard')
    return response
  }

  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)
  response.headers.set('x-debug-action', 'next')
  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Ada beberapa hal penting dari contoh ini:

  • Asset dan route internal dikecualikan supaya middleware tidak mengganggu file statis atau request internal Next.js.
  • Header debug ditambahkan untuk membantu pelacakan dari browser DevTools, log reverse proxy, atau observability tool.
  • Kondisi proteksi login simetris: dashboard butuh token, login dialihkan ke dashboard jika token sudah ada.
  • URL clone digunakan agar perubahan path dan query lebih eksplisit dan aman.

Kasus bug yang paling umum dan cara men-debug-nya

1. Redirect loop

Redirect loop biasanya terjadi ketika kondisi di middleware saling bertentangan atau terlalu luas. Contoh klasiknya:

  • User tanpa token diarahkan ke /login.
  • Namun route /login sendiri juga terkena aturan proteksi yang sama.
  • Akibatnya request ke /login kembali di-redirect ke /login atau ke lokasi lain yang akhirnya kembali lagi.

Langkah debugging:

  1. Buka tab Network di browser dan lihat rantai request 3xx.
  2. Periksa header debug seperti x-debug-action atau x-request-id.
  3. Pastikan route login, callback auth, dan asset penting tidak ikut terkena kondisi proteksi.
  4. Verifikasi bahwa kondisi isDashboard dan isLogin tidak tumpang tindih secara tidak sengaja.

Tips penting: buat kondisi yang sespesifik mungkin. Jangan hanya memakai pathname.includes('login') atau pathname.startsWith('/') tanpa batasan jelas, karena mudah menangkap route yang tidak dimaksud.

2. Header tidak terbaca

Masalah lain yang sering membingungkan adalah header yang tampaknya “hilang”. Penyebabnya bisa beberapa hal:

  • Header ditambahkan pada response, tetapi Anda mencoba membacanya dari request pada tahap yang berbeda.
  • Header diblokir atau tidak diteruskan oleh reverse proxy, CDN, atau platform hosting.
  • Nama header berbenturan dengan header standar atau tidak konsisten kapitalisasinya saat diperiksa di alat tertentu.

Prinsip pentingnya: request header datang dari klien ke server, sedangkan response header dikirim server ke klien. Menambahkan response.headers.set('x-debug', '1') tidak berarti kode server lain pada request yang sama otomatis bisa membaca x-debug sebagai request header.

Jika tujuan Anda adalah menandai alur untuk observasi manusia, response header cocok. Jika tujuan Anda adalah mengirim konteks ke service internal lain, desainnya harus mempertimbangkan batas antar-hop dengan jelas.

3. Route tertentu tidak pernah terjangkau

Gejala ini umum ketika middleware menggunakan matcher yang terlalu lebar atau rewrite yang menutupi route lain. Misalnya, semua request tanpa prefix locale di-rewrite ke /id/..., tetapi Anda lupa mengecualikan route tertentu seperti /health, /api, atau callback dari provider OAuth.

Cara men-debug:

  • Log atau tandai path asli dan aksi middleware.
  • Periksa apakah matcher menangkap route yang seharusnya dikecualikan.
  • Uji route sensitif satu per satu: /api, callback auth, webhook, health check, dan file statis.

Jika menggunakan pihak ketiga seperti identity provider atau payment gateway, callback endpoint sering rusak karena tidak dikecualikan dari middleware locale atau auth.

4. Cookie terasa tidak sinkron

Cookie sering menjadi sumber kebingungan karena ada perbedaan waktu kapan cookie dibaca dan kapan browser menyimpannya. Jika response mengirim Set-Cookie, cookie itu baru tersedia untuk request berikutnya dari browser. Jadi bila Anda berharap middleware berikutnya dalam request yang sama membaca nilai cookie yang baru diset, hasilnya bisa tidak sesuai ekspektasi.

Untuk debugging, perhatikan:

  • Apakah cookie memang ada di request saat ini?
  • Apakah cookie baru diset di response sebelumnya dan baru akan berlaku pada request berikutnya?
  • Apakah atribut cookie seperti Path, Secure, SameSite, dan domain membuatnya tidak terkirim pada request tertentu?

Teknik pelacakan request yang praktis untuk bug produksi

Di produksi, debugging lewat console.log saja biasanya tidak cukup. Anda perlu jejak yang konsisten agar satu request bisa diikuti dari awal sampai akhir.

Gunakan request ID

Tambahkan ID unik di middleware lalu kirim sebagai response header. Jika Anda juga memiliki logging di backend atau proxy, usahakan ID ini ikut dicatat. Dengan begitu, saat ada laporan “user A selalu di-redirect ke login”, Anda bisa melacak request yang relevan.

const requestId = crypto.randomUUID()
const response = NextResponse.next()
response.headers.set('x-request-id', requestId)
return response

Jika aplikasi berada di belakang load balancer atau CDN, pertimbangkan untuk mengadopsi header tracing yang sudah ada di infrastruktur Anda agar korelasi log lebih mudah.

Tambahkan header debug yang aman

Header seperti x-debug-action, x-debug-locale, atau x-debug-auth sangat membantu selama tidak membocorkan data sensitif. Hindari mengirim token, payload cookie, atau alasan internal yang terlalu detail ke klien publik.

Yang aman untuk dibagikan biasanya berupa:

  • aksi middleware: next, rewrite-locale, redirect-login
  • locale terpilih
  • route hasil pemetaan yang tidak sensitif

Log keputusan, bukan hanya nilai mentah

Daripada hanya menulis “token null”, lebih berguna mencatat keputusan final. Misalnya: “path=/id/dashboard, hasToken=false, action=redirect-login”. Log seperti ini mempercepat diagnosis karena langsung menunjukkan hubungan antara input dan keputusan.

Uji dengan curl untuk melihat header dan redirect

Browser kadang menyembunyikan detail karena otomatis mengikuti redirect. Gunakan curl untuk melihat respons mentah:

curl -I http://localhost:3000/dashboard
curl -I -L http://localhost:3000/dashboard
curl -H "Cookie: session=abc123" -I http://localhost:3000/id/dashboard

Perintah pertama melihat respons awal, perintah kedua mengikuti redirect, dan perintah ketiga mensimulasikan request dengan cookie tertentu. Ini sangat berguna untuk membuktikan apakah masalah ada pada middleware atau pada state browser pengguna.

Praktik terbaik agar middleware lebih mudah dipelihara

Buat kondisi eksplisit dan sempit

Semakin luas matcher dan aturan path, semakin sulit diprediksi efek sampingnya. Jika hanya /dashboard yang perlu proteksi, jangan menulis aturan yang secara implisit memengaruhi seluruh situs.

Pisahkan logika penentuan status

Jika middleware mulai panjang, pecah logika menjadi fungsi kecil seperti resolveLocale(), isProtectedPath(), dan shouldRedirectToLogin(). Ini membantu pengujian dan mengurangi bug akibat kondisi bercabang yang tidak terbaca.

Jangan andalkan asumsi tersembunyi tentang cookie dan header

Dokumentasikan dengan jelas siapa yang menulis cookie, kapan cookie tersedia, dan header mana yang dipakai hanya untuk observasi versus untuk kontrol alur. Banyak bug produksi berasal dari asumsi yang tidak pernah ditulis.

Siapkan daftar pengecualian route penting

Secara praktis, selalu audit route berikut saat menambahkan middleware baru:

  • /_next/*
  • /api/*
  • file statis dan favicon
  • callback auth/OAuth
  • webhook dari pihak ketiga
  • health check dan endpoint internal operasi

Penutup

Debugging middleware, header, cookie, rewrite, dan redirect di Next.js 16 pada dasarnya adalah soal memahami arah aliran request dan titik keputusan. Begitu Anda tahu kapan middleware dijalankan, kapan redirect menghentikan route awal, kapan rewrite mengubah target internal, dan kapan cookie baru benar-benar tersedia, banyak bug yang tadinya tampak misterius menjadi jauh lebih sistematis untuk ditangani.

Dalam praktik produksi, pola yang paling membantu adalah: gunakan kondisi middleware yang sempit, tambahkan request ID, kirim header debug yang aman, dan uji ulang dengan alat yang memperlihatkan respons mentah seperti DevTools dan curl. Dengan pendekatan ini, kasus seperti redirect loop, header yang tidak terbaca, atau route yang seolah tidak pernah tercapai bisa didiagnosis lebih cepat dan dengan risiko regresi yang lebih rendah.