Model caching multi-layer pada Next.js App Router dirancang agresif secara default. Kompilasi halaman dan data fetch dioptimalkan melalui empat layer: Request Memoization, Data Cache, Full Route Cache, dan Router Cache (client-side), yang sering kali digabungkan lagi dengan reverse proxy atau CDN edge cache (Cloudflare, Fastly, atau CloudFront). Ketidaksinkronan konfigurasi pada layer-layer ini dapat memicu dua insiden fatal di production: kebocoran data sensitif antarpengguna (cache bleeding) dan stale response permanen.
Akar Masalah: Konflik Layer Caching dan User Context Bleeding
Penyebab paling kritis insiden caching adalah kebocoran sesi pengguna akibat data fetch privat yang secara tidak sengaja tersimpan di Full Route Cache atau Data Cache bersama.
1. Kebocoran Konteks Pengguna (Cache Bleeding)
Ketika Server Component memanggil data profil pengguna tanpa mengakses dynamic functions (seperti cookies() atau headers()) secara eksplisit sebelum fetch, Next.js memperlakukan route tersebut sebagai static. Akibatnya, response yang dirender pertama kali oleh User A disimpan ke Full Route Cache dan disajikan ke User B.
// CONTOH SALAH: Berisiko bocor ke Full Route Cache
export default async function DashboardPage() {
// Fetch ini berpotensi masuk Data Cache global tanpa isolasi tenant/user
const res = await fetch('https://api.internal/v1/profile', {
headers: { Authorization: `Bearer ${process.env.INTERNAL_SERVICE_TOKEN}` }
});
const user = await res.json();
return <div>Email: {user.email}</div>;
}Solusi isolasi: paksa evaluasi dinamis jika route memuat data kontekstual pengguna.
// PERBAIKAN: Isolasi route ke dynamic rendering
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get('session_token')?.value;
const res = await fetch('https://api.internal/v1/profile', {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store' // Lewati Data Cache
});
const user = await res.json();
return <div>Email: {user.email}</div>;
}2. Konflik Data Cache vs CDN Edge
Next.js menyetel header Cache-Control: s-maxage=..., stale-while-revalidate=... pada output static route. Jika upstream CDN dikonfigurasi untuk menghormati header ini tanpa koordinasi invalidasi tag, maka saat runtime Next.js melakukan revalidasi via revalidateTag(), CDN tetap menyajikan salinan lama karena cache key di level edge tidak mengenali tag internal framework.
Observabilitas Caching
Identifikasi status cache secara real-time pada Next.js bergantung pada verifikasi response header internal dan structured logging.
Tracking Header x-nextjs-cache
Next.js menyematkan header diagnosa x-nextjs-cache pada response yang melewati routing server:
- HIT: Response disajikan dari Data Cache atau Full Route Cache internal tanpa memicu upstream fetch.
- MISS: Request tidak ditemukan di cache; data diambil langsung dari origin backend.
- STALE: Data kedaluwarsa disajikan sementara proses background revalidation berlangsung.
- PRERENDER: Respons disajikan dari static generation saat fase
next build. - REVALIDATED: Aset berhasil diperbarui di background dan siap disajikan pada request berikutnya.
Structured Logging dan Metrik Cache
Aktifkan logging verbose fetch pada next.config.js untuk melacak cache hit/miss ratio di environment non-local:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};
export default nextConfig;Tambahkan middleware logging untuk mengekstrak status cache ke format JSON agar dapat diparsing oleh aggregation tools (seperti Datadog, Grafana Loki, atau CloudWatch):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Tangkap metrik pada response stream
const cacheHeader = response.headers.get('x-nextjs-cache') || 'BYPASS';
console.log(JSON.stringify({
level: 'INFO',
timestamp: new Date().toISOString(),
path: request.nextUrl.pathname,
method: request.method,
cache_status: cacheHeader,
client_ip: request.headers.get('x-forwarded-for') ?? 'unknown'
}));
return response;
}Emergency Runbook: Mitigasi Insiden
Gunakan tahapan eskalasi berikut ketika terjadi insiden data leak atau stale state masif di production.
Langkah 1: Instant Purge via NEXT_DEPLOYMENT_ID
Jika menjalankan Next.js di container (misal: Kubernetes, Docker standalone), Full Route Cache dan Data Cache disk-backed dapat diputus seketika tanpa menghapus volume fisik dengan merotasi environment variable NEXT_DEPLOYMENT_ID.
# 1. Update NEXT_DEPLOYMENT_ID pada deployment manifest
kubectl set env deployment/frontend NEXT_DEPLOYMENT_ID=$(date +%s)
# 2. Pantau proses rollout pod baru
kubectl rollout status deployment/frontendNext.js menggunakan string ini sebagai namespace cache key. Mengubah nilainya otomatis mengisolasi dan mengabaikan seluruh entri Data Cache dan Full Route Cache sebelumnya.
Langkah 2: Eksekusi On-Demand Revalidation API
Jika insiden terbatas pada tag entitas data tertentu (misal: pricing-data atau catalog), hindari restart pod. Panggil endpoint internal untuk revalidasi tag secara instan.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-purge-secret');
if (secret !== process.env.PURGE_SECRET_TOKEN) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { tag, path } = await request.json();
if (tag) {
revalidateTag(tag);
return NextResponse.json({ revalidated: true, tag, at: Date.now() });
}
if (path) {
revalidatePath(path);
return NextResponse.json({ revalidated: true, path, at: Date.now() });
}
return NextResponse.json({ error: 'Missing tag or path' }, { status: 400 });
}Eksekusi via curl:
curl -X POST https://app.domain.com/api/revalidate \
-H "Content-Type: application/json" \
-H "x-purge-secret: ${PURGE_SECRET_TOKEN}" \
-d '{"tag": "products"}'Langkah 3: Pembersihan Edge/CDN Cache
Purge Next.js tidak otomatis membersihkan CDN edge. Jalankan purge edge secara terpisah via CLI penyedia CDN.
# Cloudflare: Purge Everything (Emergency Only)
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"purge_everything":true}'Strategi Rollback Aman: Mencegah Asset Desynchronization
Masalah klasik saat melakukan rollback cepat Next.js adalah ChunkLoadError. Ini terjadi ketika HTML lama hasil render merujuk pada hash file JS yang sudah dihapus pada deployment sebelumnya.
Peringatan: Jangan langsung menghapus direktori
.next/staticsaat melakukan rollback container atau deployment atomic.
Terapkan arsitektur berikut untuk menjamin keamanan rollback:
- Externalized Static Storage: Sinkronisasikan build directory
.next/static/ke Object Storage (S3/GCS) dengan retention policy minimal 72 jam. Jangan gunakan ephemeral container storage untuk chunk statis. - Immutable Cache-Control: Setel aset statis dengan header:
Cache-Control: public, max-age=31536000, immutable. Karena nama file menggunakan hash unik (misal:framework-a8f8b3.js), chunk versi lama tetap valid dan dapat diakses browser pengguna lama meskipun aplikasi sudah di-rollback ke versi sebelumnya. - Skew Protection: Gunakan Next.js Deployment Skew Protection jika menggunakan infrastruktur managed, atau arahkan request chunk ke multi-version storage bucket via reverse proxy.
Postmortem dan Tindakan Pencegahan
Setelah mitigasi selesai, pasang guardrail teknis pada pipeline CI/CD dan runtime untuk memastikan insiden tidak terulang.
1. CI Rule: Audit Cache-Control
Gunakan linting statis untuk mendeteksi pemanggilan fetch tanpa penanganan cache yang jelas di dalam server component.
// .eslintrc.json
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.name='fetch'][arguments.length=1]",
"message": "Explicit cache configuration required on fetch() in Server Components."
}
]
}
}2. Canary Guardrail: Response Header Verification
Buat automated test pada tahap synthetic monitoring canary untuk memverifikasi bahwa respons halaman privat tidak pernah mengembalikan header cache publik atau status x-nextjs-cache: HIT saat token autentikasi disertakan.
// tests/canary-cache.test.ts
import { test, expect } from '@playwright/test';
test('Private dashboard must not be cached publicly', async ({ request }) => {
const response = await request.get('/dashboard', {
headers: { Cookie: 'session_id=synthetic_test_user' }
});
const cacheControl = response.headers()['cache-control'] || '';
const nextCache = response.headers()['x-nextjs-cache'] || '';
// Verifikasi respon tidak tersimpan di public CDN
expect(cacheControl).not.toContain('public');
expect(cacheControl).toContain('private');
// Verifikasi route tidak disajikan dari shared static prerender
expect(nextCache).not.toBe('PRERENDER');
});Menerapkan pengujian response header secara otomatis di deployment pipeline mencegah kebocoran data terulang kembali ke production.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!