Middleware — satu tempat untuk tiap request
Middleware berjalan sebelum tiap halaman on-demand. Ia tempat yang benar untuk header keamanan dan konteks request — dan tempat yang salah untuk hal yang membuat halamanmu jadi personal.
Intisari
src/middleware.tsmeng-exportonRequest(context, next). Satu berkas, urutan jelas.- Ia tidak berjalan untuk halaman yang di-prerender — hanya untuk yang on-demand.
context.localsadalah cara menitipkan data ke halaman, dan ia bertipe lewatApp.Locals.sequence()merangkai beberapa middleware; urutannya persis urutan argumennya.- Bahaya: membaca cookie di middleware untuk semua rute membuat semua respons berpotensi personal. Batasi ke rute yang memang butuh.
Bentuk dasarnya
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
const mulai = Date.now();
const respons = await next();
respons.headers.set("X-Content-Type-Options", "nosniff");
respons.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
respons.headers.set("X-Waktu-Render", String(Date.now() - mulai));
return respons;
});
next() menjalankan halamannya dan mengembalikan Response. Kode sebelum
next() berjalan sebelum halaman; kode sesudahnya berjalan setelah halaman selesai. Sama seperti
middleware di framework mana pun — tapi tanpa konfigurasi urutan yang terpisah dari kodenya.
Merangkai beberapa middleware
// src/middleware.ts
import { sequence } from "astro:middleware";
import { headerKeamanan } from "@/middleware/keamanan";
import { konteksPembaca } from "@/middleware/pembaca";
import { catatAkses } from "@/middleware/log";
export const onRequest = sequence(headerKeamanan, konteksPembaca, catatAkses);
Urutannya persis urutan argumen. Tidak ada prioritas numerik, tidak ada berkas konfigurasi terpisah — apa
yang tertulis itu yang berjalan. Ini perbaikan besar dibanding hook CI3, di mana urutan hook ditentukan di
config/hooks.php dan sering tidak sama dengan yang kamu kira.
Menitipkan data lewat locals
// src/env.d.ts — memberi tipe pada locals
declare namespace App {
interface Locals {
ipPembaca: string;
idRequest: string;
member: { id: number; nama: string; langgananAktif: boolean } | null;
}
}
// src/middleware/pembaca.ts
import { defineMiddleware } from "astro:middleware";
import { randomUUID } from "node:crypto";
export const konteksPembaca = defineMiddleware(async (context, next) => {
// Di belakang Cloudflare, IP asli ada di cf-connecting-ip.
// X-Forwarded-For bisa dipalsukan klien kalau origin tidak dikunci (Fase 8).
context.locals.ipPembaca =
context.request.headers.get("cf-connecting-ip") ?? "0.0.0.0";
context.locals.idRequest =
context.request.headers.get("cf-ray") ?? randomUUID();
context.locals.member = null;
return next();
});
---
// di halaman mana pun yang on-demand
const { ipPembaca, idRequest } = Astro.locals;
---
cf-connecting-ip, bukan x-forwarded-for. Di belakang Cloudflare plus
ALB, X-Forwarded-For berisi rantai beberapa IP dan bagian depannya berasal dari klien —
artinya bisa dipalsukan. Mengambil xff.split(",")[0] untuk rate limiting berarti penyerang
bisa memalsukan IP berbeda tiap request dan melewati batasnya sepenuhnya. cf-connecting-ip
ditulis Cloudflare sendiri dan tidak bisa ditimpa klien — asalkan origin-mu tidak bisa dihubungi
langsung, yang dibahas di Fase 8.
Bahaya yang tidak jelas: middleware bisa merusak cache
// JANGAN LAKUKAN INI untuk semua rute
export const onRequest = defineMiddleware(async (context, next) => {
const sesi = context.cookies.get("sesi");
context.locals.member = sesi ? await ambilMember(sesi.value) : null;
return next();
});
Terlihat rapi, dan persis yang dilakukan CI3 dengan $this->session. Tapi konsekuensinya
persis masalah yang sedang kita perbaiki:
- Tiap request memukul database untuk mencari sesi — termasuk pembaca anonim yang tidak punya cookie sama sekali. Di 500 ribu request per jam, itu 500 ribu kueri yang tidak perlu.
- Kalau ada satu saja komponen yang merender
locals.member, halamannya jadi personal — dan cache edge harus dimatikan untuk seluruh rute itu. - Kalau kamu lupa mematikan cache-nya, jauh lebih buruk: halaman satu member bisa tersaji ke member lain.
Yang benar: middleware hanya menyiapkan cara membaca sesi; yang benar-benar membacanya adalah server island dan rute yang memang personal.
const RUTE_PERSONAL = ["/akun", "/langganan", "/_server-islands"];
export const konteksPembaca = defineMiddleware(async (context, next) => {
const perlu = RUTE_PERSONAL.some((p) => context.url.pathname.startsWith(p));
context.locals.member = perlu ? await memberDariCookie(context.cookies) : null;
const respons = await next();
// Sabuk pengaman: apa pun yang personal tidak boleh di-cache bersama,
// bahkan kalau halamannya lupa mengaturnya sendiri.
if (perlu) {
respons.headers.set("Cache-Control", "private, no-store");
}
return respons;
});
Blok terakhir itu pertahanan berlapis, dan layak ada. Halaman baru akan ditambahkan oleh orang
yang tidak membaca materi ini. Kalau satu-satunya yang mencegah kebocoran adalah setiap penulis halaman
mengingat untuk menulis private, no-store, cepat atau lambat ada yang lupa. Pasang pagarnya
di satu tempat.
Menghentikan request lebih awal
export const jagaAkun = defineMiddleware(async (context, next) => {
if (context.url.pathname.startsWith("/akun")) {
const member = await memberDariCookie(context.cookies);
if (!member) {
const tujuan = encodeURIComponent(context.url.pathname);
return context.redirect(`/masuk?tujuan=${tujuan}`, 302);
}
context.locals.member = member;
}
return next();
});
Mengembalikan Response tanpa memanggil next() menghentikan rantainya —
halamannya tidak pernah berjalan. Ini padanan redirect(); exit; di hook CI3, tapi tanpa
exit yang mematikan proses.
Perhatikan 302, bukan 301. Redirect ke halaman login harus sementara.
Browser meng-cache 301 secara permanen — kalau kamu memakainya, pembaca yang sudah login pun akan terus
dilempar ke halaman masuk oleh cache browsernya sendiri, dan satu-satunya perbaikan adalah menyuruh
mereka membersihkan cache. Ini kesalahan yang mahal dan sulit dilacak.
Yang perlu diingat
| Fakta | Akibatnya |
|---|---|
| Tidak berjalan untuk halaman prerender | Header keamanan di halaman statis harus diatur di Cloudflare atau nginx |
| Berjalan untuk endpoint API juga | Webhook Midtrans ikut melewatinya — jangan pasang pengecekan sesi di jalur itu |
| Berjalan untuk request server island | Jalur /_server-islands/ perlu ikut dianggap personal |
Tidak berjalan untuk berkas di public/ | Aset statis dilayani sebelum middleware |
| Error di middleware = seluruh situs 500 | Bungkus operasi yang bisa gagal dengan try/catch |
Latihan: tulis middleware berantai memakai sequence(): satu memasang header
keamanan, satu mengisi locals.idRequest, satu mencatat metode, jalur, status, dan durasi
ke console.log dalam bentuk JSON satu baris. Beri tipe App.Locals di
env.d.ts. Lalu buktikan batasannya: tandai satu halaman prerender = true,
buka halaman itu, dan konfirmasi tidak ada baris log yang muncul untuknya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.