Vercel Edge Runtime: Middleware avansat, geolocalizare și testare A/B
Cu Vercel, middleware-ul edge realizează mai întâi randarea SSR, permițând rutarea bazat pe geolocalizare, semnalizări de caracteristici, testare A/B și autentificare fără pornire la rece, totul în mai puțin de 50 ms în medie globală.
Middleware înainte de randare
Într-o aplicație tradițională Next.js implementată pe Vercel, fiecare solicitare ajunge mai întâi la CDN, apoi — dacă nu este în cache — este redirecționat către serverul SSR. Middleware-ul Edge se încadrează între aceste două etape: vine efectuate la marginea rețelei Vercel, înainte de rutare către serverul SSR și înainte ca răspunsul să fie servit din cache.
Această poziționare este critică: middleware-ul se poate modifica solicitarea, redirecționarea utilizatorului, setarea cookie-urilor sau antete și toate acestea se întâmplă într-un timp de rulare bazat pe V8, cu pornire la rece aproape de zero. Măsuri Vercel o latență de P50 < 10 ms pentru majoritatea middleware-urilor din lor regiuni marginale (100+ la nivel global).
Ce vei învăța
- Structura middleware și convenția de fișier în Next.js App Router
- Geolocalizare: rutare în funcție de țară, regiune și limbă
- Testare A/B stabilă cu cookie-uri persistente
- Prezintă steaguri la margine fără călătorii dus-întors la server
- Autentificare ușoară în middleware
- Limitări ale Vercel Edge Runtime în comparație cu Node.js
- Depanare locală middleware cu
next dev
Dosarul middleware.ts
În Next.js (App Router și Pages Router), middleware-ul este definit în fișier
middleware.ts (o .js) în rădăcina proiectului.
Funcția exportată primește un obiect NextRequest și trebuie să dea înapoi
a NextResponse:
// middleware.ts - struttura base
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// La funzione middleware e chiamata per ogni richiesta che corrisponde al matcher
export function middleware(request: NextRequest): NextResponse {
// request.nextUrl: URL della richiesta (con searchParams, pathname, etc.)
// request.headers: header HTTP
// request.cookies: cookie della richiesta
// request.geo: geolocalizzazione (solo su Vercel, non in local dev)
// request.ip: IP del client
const response = NextResponse.next(); // Prosegui senza modifiche
// Aggiunge un header custom alla risposta
response.headers.set('X-Middleware-Applied', 'true');
return response;
}
// Configura su quali path il middleware viene eseguito
// IMPORTANTE: specificare il matcher migliora le performance
// evitando l'esecuzione per asset statici (_next/static, immagini, etc.)
export const config = {
matcher: [
// Esegui su tutte le route eccetto quelle statiche e API
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Geolocalizare: rutare după țară și limbă
Vercel îmbogățește fiecare cerere cu informații geografice prin intermediul subiectului
request.geo. În producție (nu în dezvoltarea locală), acestea sunt disponibile
tara, regiune si oras:
// middleware.ts - redirect geografico per lingua
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Mappa paese -> locale preferito
const COUNTRY_LOCALE_MAP: Record<string, string> = {
IT: 'it',
DE: 'de',
FR: 'fr',
ES: 'es',
PT: 'pt',
BR: 'pt-BR',
// Default: 'en' per tutti gli altri paesi
};
const SUPPORTED_LOCALES = ['en', 'it', 'de', 'fr', 'es', 'pt'];
const DEFAULT_LOCALE = 'en';
export function middleware(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Non processare le route che gia hanno un locale nel path
// Esempio: /it/dashboard non deve essere reindirizzato
const pathLocale = pathname.split('/')[1];
if (SUPPORTED_LOCALES.includes(pathLocale)) {
return NextResponse.next();
}
// Determina il locale preferito
// Priorita: 1) Cookie impostato dall'utente, 2) Paese IP, 3) Accept-Language, 4) Default
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
return rewriteWithLocale(request, cookieLocale);
}
const country = request.geo?.country ?? 'US';
const geoLocale = COUNTRY_LOCALE_MAP[country] ?? DEFAULT_LOCALE;
// Verifica Accept-Language come fallback
const acceptLanguage = request.headers.get('Accept-Language') ?? '';
const browserLocale = parseAcceptLanguage(acceptLanguage, SUPPORTED_LOCALES);
const finalLocale = geoLocale !== DEFAULT_LOCALE
? geoLocale
: (browserLocale ?? DEFAULT_LOCALE);
return rewriteWithLocale(request, finalLocale);
}
function rewriteWithLocale(request: NextRequest, locale: string): NextResponse {
const url = request.nextUrl.clone();
url.pathname = `/${locale}${url.pathname}`;
// Rewrite (non redirect) per mantenere l'URL originale nel browser
// L'utente vede /dashboard ma viene servita /it/dashboard
return NextResponse.rewrite(url);
}
function parseAcceptLanguage(header: string, supported: string[]): string | null {
// Parsa "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7"
const languages = header
.split(',')
.map((lang) => {
const [code, q] = lang.trim().split(';q=');
return { code: code.split('-')[0].toLowerCase(), q: parseFloat(q ?? '1') };
})
.sort((a, b) => b.q - a.q);
for (const { code } of languages) {
if (supported.includes(code)) return code;
}
return null;
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico|.*\\.\\w+).*)'],
};
Testare A/B stabilă cu cookie-uri
Testarea A/B la margine necesită ca fiecare utilizator să rămână în propriul grup pe tot parcursul sesiunii (sau mai mult). Soluția standard este atribuiți grupul la prima conectare și păstrați alegerea într-un cookie:
// middleware.ts - A/B testing per homepage
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const AB_COOKIE = 'ab_homepage';
const AB_VARIANTS = ['control', 'variant_a', 'variant_b'] as const;
type AbVariant = typeof AB_VARIANTS[number];
// Distribuzione del traffico: 50% control, 25% variant_a, 25% variant_b
const WEIGHTS = [0.5, 0.25, 0.25];
export function middleware(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Applica A/B test solo sulla homepage
if (pathname !== '/') {
return NextResponse.next();
}
const response = NextResponse.next();
// Controlla se l'utente ha gia un gruppo assegnato
let variant = request.cookies.get(AB_COOKIE)?.value as AbVariant | undefined;
if (!variant || !AB_VARIANTS.includes(variant as AbVariant)) {
// Assegna un nuovo gruppo deterministicamente
variant = assignVariant(request);
// Salva il gruppo nel cookie (1 mese di durata)
response.cookies.set(AB_COOKIE, variant, {
maxAge: 30 * 24 * 60 * 60, // 30 giorni
path: '/',
httpOnly: false, // Visibile a JavaScript per il tracking
sameSite: 'lax',
});
}
// Aggiunge il gruppo come header per il server component e analytics
response.headers.set('X-AB-Variant', variant);
// Rewrite verso la variante corretta
if (variant !== 'control') {
const url = request.nextUrl.clone();
url.pathname = `/experiments/homepage/${variant}`;
return NextResponse.rewrite(url, { headers: response.headers });
}
return response;
}
function assignVariant(request: NextRequest): AbVariant {
// Usa l'IP + User-Agent per un'assegnazione deterministica (non cambia tra ricariche)
const seed = `${request.ip ?? ''}${request.headers.get('User-Agent') ?? ''}`;
const hash = simpleHash(seed);
const normalized = (hash % 1000) / 1000; // Normalizza a [0, 1)
let cumulative = 0;
for (let i = 0; i < WEIGHTS.length; i++) {
cumulative += WEIGHTS[i];
if (normalized < cumulative) return AB_VARIANTS[i];
}
return AB_VARIANTS[0]; // Fallback al control
}
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // Converte a 32-bit integer
}
return Math.abs(hash);
}
export const config = {
matcher: ['/'],
};
Prezentați steaguri la margine
Indicatoarele de caracteristici vă permit să activați funcții pentru subseturi de utilizatori fără o nouă implementare. Cu edge middleware, puteți citi steaguri dintr-o configurație edge (Vercel Edge Config) și aplicați-le înainte de randare:
// middleware.ts - feature flags con Vercel Edge Config
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';
interface FeatureFlags {
newDashboard: boolean;
betaFeatures: string[]; // Lista di user IDs con accesso beta
maintenanceMode: boolean;
}
export async function middleware(request: NextRequest): Promise<NextResponse> {
// Edge Config ha latenza < 1ms (replicated globally)
// get() e sincrono in termini di latenza percepita
const flags = await get<FeatureFlags>('featureFlags');
// Modalita manutenzione globale
if (flags?.maintenanceMode) {
const url = request.nextUrl.clone();
url.pathname = '/maintenance';
return NextResponse.rewrite(url);
}
// Reindirizza alla nuova dashboard se il flag e attivo
if (flags?.newDashboard && request.nextUrl.pathname === '/dashboard') {
const url = request.nextUrl.clone();
url.pathname = '/dashboard-v2';
return NextResponse.rewrite(url);
}
// Accesso beta: controlla se l'utente e nella lista
const userId = request.cookies.get('user_id')?.value;
if (userId && flags?.betaFeatures?.includes(userId)) {
const response = NextResponse.next();
response.headers.set('X-Beta-User', 'true');
return response;
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next|api|.*\\.\\w+).*)'],
};
Autentificare ușoară în Middleware
Middleware-ul este un loc minunat pentru a verifica JWT și a redirecționa utilizatori neautentificați înainte ca cererea să ajungă la server:
// middleware.ts - verifica JWT leggera all'edge
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_PATHS = ['/login', '/register', '/api/auth', '/_next', '/favicon.ico'];
const AUTH_COOKIE = 'session_token';
export async function middleware(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
// Salta le route pubbliche
if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
return NextResponse.next();
}
const token = request.cookies.get(AUTH_COOKIE)?.value;
if (!token) {
return redirectToLogin(request);
}
try {
// Verifica il JWT usando Web Crypto API (disponibile nell'edge runtime)
const isValid = await verifyJwt(token, request.headers.get('x-jwt-secret') ?? '');
if (!isValid) {
return redirectToLogin(request);
}
return NextResponse.next();
} catch {
return redirectToLogin(request);
}
}
async function verifyJwt(token: string, secret: string): Promise<boolean> {
try {
const [headerB64, payloadB64, signatureB64] = token.split('.');
if (!headerB64 || !payloadB64 || !signatureB64) return false;
// Verifica la firma con HMAC-SHA256
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signature = base64UrlDecode(signatureB64);
const data = encoder.encode(`${headerB64}.${payloadB64}`);
const isValid = await crypto.subtle.verify('HMAC', key, signature, data);
if (!isValid) return false;
// Verifica la scadenza
const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
if (payload.exp && payload.exp < Date.now() / 1000) return false;
return true;
} catch {
return false;
}
}
function base64UrlDecode(str: string): ArrayBuffer {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function redirectToLogin(request: NextRequest): NextResponse {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.\\w+).*)'],
};
Limitări ale Vercel Edge Runtime
Vercel Edge Runtime se bazează pe V8 cu un subset al API-ului Node.js. Principalele limitări de care trebuie să fii conștient:
| Caracteristică | Edge Runtime | Runtime Node.js |
|---|---|---|
| Pornire la rece | < 5 ms | ~50-500 ms |
| Timeout maxim | 30 de ani (pe Vercel Pro) | 15 min (pe Vercel Enterprise) |
| Memoria maxima | 128 MB | 3 GB |
| modulul fs (sistem de fișiere) | Nu este disponibil | Disponibil |
| Node.js încorporat (cale, nod cripto) | Parţial | Complet |
| Web Crypto API | Disponibil | Disponibil (de la Nodul 17) |
| Preluați API-ul | Nativ | Nativ (de la Nodul 18) |
| pachete npm cu legături native | Nu este acceptat | Sprijinit |
Biblioteci incompatibile cu Edge Runtime
Multe biblioteci populare npm nu sunt compatibile cu Vercel Edge Runtime:
Prisma (folosește drivere native), bcrypt (legare nativă), sharp (imagini native).
Înainte de a utiliza o bibliotecă în middleware, verificați dacă aceasta acceptă edge runtime.
Prisma are un adaptor compatibil cu margini (@prisma/adapter-pg cu
@neondatabase/serverless), dar nu toate caracteristicile sunt disponibile.
Depanare și testare middleware
Middleware-ul este testabil local cu next dev.
Câteva limitări: request.geo este întotdeauna undefined
local (folosește o simulare), iar Edge Config necesită variabila de mediu
EDGE_CONFIG:
// middleware.ts con mock per development locale
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
function getGeo(request: NextRequest): { country?: string; region?: string } {
// In development, simula la geolocalizzazione tramite header custom
// Utile per testare diverse regioni senza deploy
if (process.env.NODE_ENV === 'development') {
return {
country: request.headers.get('X-Mock-Country') ?? 'US',
region: request.headers.get('X-Mock-Region') ?? 'CA',
};
}
return {
country: request.geo?.country,
region: request.geo?.region,
};
}
export function middleware(request: NextRequest): NextResponse {
const { country, region } = getGeo(request);
// Logica di routing geo-based
const response = NextResponse.next();
response.headers.set('X-Country', country ?? 'unknown');
response.headers.set('X-Region', region ?? 'unknown');
return response;
}
Concluzii și pașii următori
Vercel middleware este unul dintre cele mai puternice instrumente pentru personalizați comportamentul unei aplicații Next.js fără modificări logica pe partea serverului. Poziția sa la margine — înainte de randare și cache — îl face ideal pentru decizii rapide de rutare care nu necesită acces la baza de date.
Modelele afișate (geolocalizare, testare A/B, semnalizatoare de caracteristici, autentificare) acestea acoperă majoritatea cazurilor de utilizare obișnuite. Cheia succesului este mențineți middleware-ul ușor: logică complexă sau accese lente la baze de date aparțin componentelor serverului sau gestionatorilor de rute.
Următoarele articole din serie
- Articolul 7: Rutarea geografică la margine — Personalizare Conformitatea conținutului și GDPR: geo-fencing cu Cloudflare Workers, prețuri localizate și blocați din cauza reglementărilor locale.
- Articolul 8: API-ul cache și strategiile de invalidare în Cloudflare Lucrători: învechit-în timp ce-revalidează și invalidare prin cheie în producție.
- Articolul 9: Testarea lucrătorilor în local — Miniflare, Vitest și Wrangler Dev: flux de lucru complet pentru testare fără implementare.







