Insiden loop infinite reload pada aplikasi berbasis Inertia.js kerap muncul tanpa peringatan di lingkungan produksi sesaat setelah proses rolling deployment berjalan. Pengguna mendapati peramban melakukan hard refresh terus-menerus pada antarmuka, sementara metrik server mencatat lonjakan drastis pada status kode HTTP 409 Conflict. Masalah ini berakar pada ketidaksinkronan mekanisme asset versioning antara klien dan kluster server.

Gejala Bug dan Dampak Sistem

Anomali bermula ketika klien mencoba melakukan navigasi SPA (Single Page Application). Alih-alih merender halaman tujuan via XHR, peramban memuat ulang seluruh halaman (hard reload) secara berulang tanpa henti. Kondisi ini memicu beberapa indikasi kritis pada infrastruktur:

  • Lonjakan HTTP 409 Conflict: Log akses Nginx atau load balancer mencatat ribuan respons 409 dalam hitungan menit untuk endpoint yang biasanya merespons dengan 200 OK.
  • Beban Ekstrem pada Load Balancer: Setiap 409 memicu full document request baru, yang kemudian mengirim ulang request navigasi Inertia, melipatgandakan traffic secara eksponensial.
  • Session Churn: Lonjakan pemuatan ulang halaman penuh meningkatkan utilisasi CPU pod backend serta membebani penyimpanan sesi (Redis/Database).

Mekanisme Protokol Inertia dan Root Cause

Inertia.js menggunakan sistem asset versioning bawaan untuk menjamin bahwa aset JavaScript/CSS pada peramban pengguna selalu identik dengan kode yang dijalankan server backend.

Ketika klien melakukan navigasi, Inertia menyertakan header X-Inertia: true beserta X-Inertia-Version: <hash>. Middleware backend (umumnya HandleInertiaRequests pada Laravel) mengevaluasi header ini melalui method version():

public function version(Request $request): ?string
{
    return parent::version($request);
}

Implementasi bawaan parent::version($request) membaca hash dari file manifest Vite atau Mix. Jika header X-Inertia-Version dari klien tidak cocok dengan nilai yang dihasilkan backend saat menerima request GET, Inertia backend memutus siklus respons data dan merespons dengan:

  1. Status HTTP 409 Conflict.
  2. Header X-Inertia-Location: <target-url>.

Sisi klien merespons status 409 ini dengan mengeksekusi window.location.href = response.headers['x-inertia-location'] untuk memaksa peramban mengunduh ulang HTML, file CSS, dan bundle JavaScript terbaru.

Anatomi Terjadinya Infinite Loop

Loop tak berujung terjadi saat aplikasi berjalan di balik Load Balancer dengan beberapa instance (Pod/Container) selama atau setelah proses deployment:

  1. Skenario Rolling Deployment: Pod A masih menjalankan Commit-v1, sementara Pod B telah diperbarui ke Commit-v2.
  2. Klien memuat aplikasi dari Pod A, menyimpan cache aset dan versi v1.
  3. Pengguna mengeklik tautan internal. Request navigasi Inertia (membawa header X-Inertia-Version: v1) dialihkan oleh Load Balancer ke Pod B.
  4. Pod B mendeteksi perbedaan versi (v1 != v2), menolak request dengan status 409 Conflict, dan menyertakan header X-Inertia-Location.
  5. Klien melakukan hard reload. Sialnya, request pemuatan dokumen HTML ini dialihkan kembali oleh Load Balancer ke Pod A yang masih menjalankan v1.
  6. Peramban kembali memuat bundle aset versi v1. Navigasi berikutnya kembali membentur Pod B. Pola ini berulang tanpa henti hingga deployment selesai secara penuh atau cache load balancer stabil.

Peringatan Tambahan: Masalah serupa tetap dapat terjadi pada pod yang identik jika version() dihitung menggunakan filemtime(public_path('build/manifest.json')). Jika build asset dijalankan secara lokal di tiap container pada waktu berbeda, nilai timestamp file akan berbeda antar-pod meskipun kode sumbernya sama persis.

Investigasi: Replikasi dan Pembuktian Header

Untuk memverifikasi inkonsistensi versi antar-server, lakukan simulasi request Inertia menggunakan cURL langsung ke IP atau hostname masing-masing pod di balik load balancer.

Kirimkan request dengan menyertakan header versi lama:

curl -i -X GET "https://app.internal/dashboard" \
  -H "X-Inertia: true" \
  -H "X-Inertia-Version: 39a48d88fa7e6616e6d5e1ff7bbd8f8e" \
  -H "Accept: text/html, application/xhtml+xml"

Periksa respons yang dihasilkan. Instance yang memiliki versi berbeda akan menghasilkan respons berikut:

HTTP/2 409
date: Mon, 24 Mar 2025 04:12:01 GMT
x-inertia-location: https://app.internal/dashboard
content-type: text/html; charset=UTF-8

Jika verifikasi dilakukan melalui peramban, buka tab Network pada DevTools, aktifkan opsi Preserve log, lalu amati rantai request. Baris merah berstatus 409 akan segera disusul oleh request tipe document berulang kali dengan URL yang sama.

Langkah Remediasi

Solusi teknis mencakup perbaikan cara deklarasi versi di middleware aplikasi dan standardisasi pipeline CI/CD.

1. Gunakan Identifier Deterministik Statis

Hindari komputasi dinamis atau ketergantungan pada timestamp file lokal container. Gunakan Git Commit SHA atau Release Tag unik yang disuntikkan via environment variable saat proses CI/CD.

Ubah method version() pada app/Http/Middleware/HandleInertiaRequests.php:

--- a/app/Http/Middleware/HandleInertiaRequests.php
+++ b/app/Http/Middleware/HandleInertiaRequests.php
@@ -22,6 +22,6 @@ class HandleInertiaRequests extends Middleware
     public function version(Request $request): ?string
     {
-        return parent::version($request);
+        return config('app.asset_version') ?? parent::version($request);
     }
 }

Daftarkan konfigurasi pada config/app.php:

'asset_version' => env('APP_ASSET_VERSION', null),

Pada pipeline deployment (misalnya GitHub Actions atau GitLab CI), tetapkan nilai variabel lingkungan sebelum image di-build:

APP_ASSET_VERSION=${{ github.sha }}

2. Build Asset Terpusat Sekali di CI

Jangan menjalankan npm run build secara terpisah di masing-masing runner pod. Eksekusi build aset satu kali pada fase pipeline integrasi, lalu salin direktori public/build yang identik ke seluruh image kontainer target. Dengan demikian, hash manifest bersifat absolut di semua pod.

3. Mitigasi Infrastruktur dan Deployment Strategy

  • Rolling Update Health Check: Pastikan pod lama tidak menerima request sebelum pod baru sepenuhnya sehat, dan perpendek drain window jika terjadi divergensi aset yang masif.
  • Sticky Sessions (Opsional): Jika proses rolling deployment memerlukan waktu lama akibat skala kluster yang masif, gunakan session affinity (sticky cookies) pada ingress controller untuk memastikan satu klien tetap terhubung ke node yang sama sampai deployment selesai.

Mengganti kalkulasi dinamis dengan versi deterministik berbasis SHA commit serta menyamakan artefak build di seluruh node secara tuntas menghapus risiko infinite 409 loop pada aplikasi Inertia.js.