Postmortem Insiden: Kasus Infinite Reload Loop Respons HTTP 409

Pada arsitektur monolit berbasis Inertia.js yang berjalan di atas cluster multi-node (seperti Kubernetes atau AWS ECS), rolling deployment tanpa konfigurasi aset yang sinkron memicu insiden kritis: pengguna terjebak dalam siklus penyegaran halaman tanpa henti (infinite reload loop).

Gejala dimulai sesaat setelah rolling update berjalan parsial. Pengguna yang sedang membuka aplikasi mengeklik tautan internal. Browser menerima respons status HTTP 409 Conflict, melakukan refresh halaman penuh secara otomatis, lalu kembali menerima status 409 pada interaksi berikutnya. Kondisi ini membuat aplikasi tidak dapat digunakan sama sekali dan membebani server backend akibat lonjakan request navigasi.

Mekanisme Internal Inertia.js

Inertia.js menggunakan strategi asset versioning untuk memastikan aset klien (JavaScript dan CSS bundle) selalu sinkron dengan payload HTML/JSON yang dikirimkan server. Alur standarnya adalah sebagai berikut:

  1. Klien mengirim permintaan AJAX/Fetch dengan header X-Inertia: true dan menyertakan header X-Inertia-Version: <client_version>.
  2. Middleware backend (HandleInertiaRequests) memeriksa apakah nilai X-Inertia-Version dari klien identik dengan nilai kembalian fungsi Inertia::version() di server.
  3. Jika nilainya berbeda, server menolak permintaan dengan status HTTP 409 Conflict dan menyertakan header X-Inertia-Location: <current_url>.
  4. Pustaka klien Inertia menangkap status 409, membaca header X-Inertia-Location, lalu mengeksekusi window.location.href = xInertiaLocation untuk memicu hard-refresh browser agar aset terbaru diunduh.

Kegagalan fatal terjadi saat request diarahkan bolak-balik oleh load balancer ke node yang menjalankan versi aplikasi berbeda selama masa rolling deployment.

Root Cause Analysis: Version Drift Antar-Node

Akar masalah insiden ini terbagi menjadi dua faktor teknis utama: distribusi trafik pada status transisi aplikasi dan metode kalkulasi versi aset.

1. Version Drift pada Transisi Rolling Deploy

Dalam rolling deploy dengan round-robin load balancer, pod/instans lama (Versi A) dan pod/instans baru (Versi B) aktif berdampingan. Skenario loop terjadi melalui tahapan ini:

  • Browser klien memegang memori frontend Versi A.
  • Klien melakukan navigasi via AJAX; load balancer meneruskan request ke Node 2 (menjalankan Versi B).
  • Node 2 mendeteksi perbedaan versi (A vs B), lalu mengembalikan status 409 Conflict.
  • Klien Inertia mengeksekusi hard-refresh (GET /dashboard non-AJAX).
  • Load balancer mengarahkan hard-refresh tersebut ke Node 1 (yang masih menjalankan Versi A). Klien memuat ulang aset Versi A.
  • Ketika pengguna mengeklik tautan berikutnya, request kembali masuk ke Node 2 (Versi B), memicu respons 409 baru. Proses berulang tanpa henti.

2. Kalkulasi Hash Runtime Lokal

Banyak implementasi default Inertia pada Laravel mengandalkan kalkulasi runtime lokal di dalam HandleInertiaRequests.php:

public function version(Request $request): ?string
{
    // Anti-pattern pada cluster multi-node
    return md5_file(public_path('build/manifest.json'));
}

Jika image container atau server di-build secara terpisah pada masing-masing mesin, perbedaan kecil pada timestamp atau tool bundler (misalnya Vite/Mix) dapat menghasilkan checksum manifest yang berbeda meski kode sumbernya identik. Hal ini menyebabkan dua node pada rilis yang sama tetap memiliki versi hash yang tidak cocok satu sama lain.

Observabilitas dan Deteksi Anomali

Mendeteksi anomali ini membutuhkan metrik khusus di layer reverse proxy dan Application Performance Monitoring (APM).

Deteksi Log Nginx / Ingress Controller

Pada kondisi normal, volume respons 409 hanya muncul sesaat per pengguna unik ketika terjadi rilis. Jika grafik log menunjukkan lonjakan berkelanjutan dengan URI identik dari IP/sesi yang sama, sistem mengalami loop.

# Contoh log Nginx yang mengindikasikan 409 berulang
192.168.1.10 - - [24/May/2024:10:15:32 +0000] "GET /dashboard HTTP/2.0" 409 0 "https://app.example.com/" "Mozilla/5.0..." X-Inertia-Version: 3f8a1c9
192.168.1.10 - - [24/May/2024:10:15:33 +0000] "GET /dashboard HTTP/2.0" 200 4521 "https://app.example.com/" "Mozilla/5.0..."
192.168.1.10 - - [24/May/2024:10:15:34 +0000] "GET /invoices HTTP/2.0" 409 0 "https://app.example.com/" "Mozilla/5.0..." X-Inertia-Version: 3f8a1c9

Prometheus Alert Rule

Pasang metrik pemantauan tingkat error status 409 pada Ingress (seperti Nginx Ingress Controller):

- alert: InertiaVersionLoopHigh409Rate
  expr: sum(rate(nginx_ingress_controller_requests{status="409"}[2m])) by (ingress) 
        / sum(rate(nginx_ingress_controller_requests[2m])) by (ingress) * 100 > 5
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "Lonjakan HTTP 409 terdeteksi pada Ingress {{ $labels.ingress }}"
    description: "Rasio respons HTTP 409 melebihi 5% selama 1 menit, mengindikasikan adanya version drift atau deploy loop di Inertia.js."

Langkah Mitigasi Darurat

Jika insiden sedang terjadi di lingkungan produksi, lakukan intervensi segera:

  1. Hentikan Rolling Update: Batalkan proses rollout pipeline CI/CD agar tidak menambah variasi node aktif.
  2. Safe Rollback atau Fast-Forward: Kembalikan target replica set seluruhnya ke revisi deployment sebelumnya, atau dorong seluruh pod ke versi baru sekaligus. Jangan biarkan cluster berada dalam rasio node campuran (misal: 50% versi lama, 50% versi baru).
  3. Traffic Pinning Sementara: Jika infrastruktur mendukung Ingress Sticky Sessions, aktifkan sementara cookie affinity agar setiap browser pengguna terkunci pada satu pod yang sama hingga proses migrasi selesai.

Tindakan Pencegahan dan Standardisasi Arsitektur

Untuk menghilangkan risiko version drift secara permanen, terapkan dua pendekatan berikut pada sistem deployment.

1. Standarisasi Version Provider via Git Commit SHA

Hilangkan kalkulasi lokal berbasis file di runtime. Masukkan commit hash identik ke dalam environment variable saat tahap build CI/CD.

// app/Http/Middleware/HandleInertiaRequests.php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function version(Request $request): ?string
    {
        // Mengambil identifier statis yang diinjeksikan saat build pipeline
        return config('app.asset_version');
    }
}

Daftarkan konfigurasi pada config/app.php:

'asset_version' => env('APP_DEPLOY_VERSION', 'local-dev'),

Pada Dockerfile atau CI/CD pipeline, pasang nilai Git SHA tunggal:

ARG GIT_SHA
ENV APP_DEPLOY_VERSION=${GIT_SHA}
RUN test -n "$GIT_SHA" || (echo "GIT_SHA harus disediakan saat build" && exit 1)

2. Implementasi Blue-Green Deployment

Rolling deployment murni pada arsitektur monolitik yang menggabungkan stateful web session dan bundle frontend dinamis memiliki kelemahan mendasar pada masa transisi. Solusi paling stabil untuk Inertia.js adalah strategi Blue-Green Deployment:

  • Lingkungan Blue (aktif) melayani seluruh lalu lintas produksi.
  • Lingkungan Green (baru) di-deploy secara terpisah hingga seluruh pod berstatus healthy.
  • Aset frontend dari lingkungan Green diunggah ke Object Storage (S3 / CDN) sebelum switch-over terjadi. Penting: aset versi Blue harus tetap dipertahankan di CDN selama beberapa waktu agar klien lama yang belum merefresh browser tidak mengalami 404 Not Found saat memuat lazy chunk.
  • Pindahkan 100% traffic dari Blue ke Green pada layer load balancer dalam satu operasi atomik.

Pola ini memastikan tidak ada kondisi di mana seorang pengguna mengakses node versi A dan node versi B secara berselang-seling, mengeliminasi loop HTTP 409 pada aplikasi Inertia.js.