Oprogramowanie pośrednie przed renderowaniem

W tradycyjnej aplikacji Next.js wdrożonej na platformie Vercel każde żądanie najpierw dociera do CDN, a następnie — jeśli nie znajduje się w pamięci podręcznej — jest przekazywany dalej do serwera SSR. Oprogramowanie pośrednie Edge mieści się pomiędzy tymi dwoma etapami: przychodzi wykonywane na brzegu sieci Vercel, przed routingiem do serwera SSR i przed dostarczeniem odpowiedzi z pamięci podręcznej.

To umiejscowienie jest krytyczne: oprogramowanie pośrednie może modyfikować żądanie, przekierowanie użytkownika, ustawienie plików cookie lub nagłówków i to wszystko dzieje się w środowisku wykonawczym opartym na V8 z zimnym startem bliskim zera. środki Vercela opóźnienie P50 < 10 ms dla większości oprogramowania pośredniego w ich regiony brzegowe (ponad 100 na całym świecie).

Czego się nauczysz

  • Struktura oprogramowania pośredniczącego i konwencja plików w routerze aplikacji Next.js
  • Geolokalizacja: wyznaczanie trasy według kraju, regionu i języka
  • Stabilne testy A/B z trwałymi plikami cookie
  • Flagi funkcyjne na krawędzi bez podróży w obie strony do serwera
  • Lekkie uwierzytelnianie w oprogramowaniu pośrednim
  • Ograniczenia Vercel Edge Runtime w porównaniu z Node.js
  • Lokalne debugowanie oprogramowania pośredniego za pomocą next dev

Plik middleware.ts

W Next.js (App Router i Pages Router) oprogramowanie pośredniczące jest zdefiniowane w pliku middleware.ts (o .js) w katalogu głównym projektu. Eksportowana funkcja otrzymuje obiekt NextRequest i musi oddać 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)$).*)',
  ],
};

Geolokalizacja: wyznaczanie trasy według kraju i języka

Vercel wzbogaca każde zapytanie o informacje geograficzne poprzez temat request.geo. W produkcji (nie u lokalnego dewelopera) są dostępne kraj, region i miasto:

// 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+).*)'],
};

Stabilne testy A/B z plikami cookie

Testowanie A/B na brzegu wymaga od każdego użytkownika pozostania w swojej własnej grupie przez całą sesję (lub dłużej). Standardowym rozwiązaniem jest przypisz grupę przy pierwszym logowaniu i zapisz wybór w pliku 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: ['/'],
};

Flagi funkcyjne na krawędzi

Flagi funkcji umożliwiają włączenie funkcji dla podzbiorów użytkowników bez nowego wdrożenia. Dzięki oprogramowaniu pośredniczącemu Edge można odczytywać flagi z konfiguracji brzegowej (Vercel Edge Config) i zastosuj je przed renderowaniem:

// 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+).*)'],
};

Lekkie uwierzytelnianie w oprogramowaniu pośrednim

Oprogramowanie pośrednie to świetne miejsce do weryfikacji JWT i przekierowania nieuwierzytelnieni użytkownicy, zanim żądanie dotrze do serwera:

// 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+).*)'],
};

Ograniczenia środowiska wykonawczego Vercel Edge

Środowisko wykonawcze Vercel Edge jest oparte na wersji V8 z podzbiorem interfejsu API Node.js. Główne ograniczenia, o których należy pamiętać:

Charakterystyczny Środowisko wykonawcze Edge Środowisko wykonawcze Node.js
Zimny ​​start < 5 ms ~50-500ms
Maksymalny limit czasu 30s (na Vercel Pro) 15 minut (w programie Vercel Enterprise)
Maksymalna pamięć 128MB 3 GB
moduł fs (system plików) Niedostępne Dostępny
Wbudowane rozwiązania Node.js (ścieżka, węzeł kryptograficzny) Częściowy Kompletny
Internetowy interfejs API kryptowalut Dostępny Dostępne (od węzła 17)
Pobierz API Rodzinny Natywny (z węzła 18)
pakiety npm z natywnymi powiązaniami Nieobsługiwane Utrzymany

Biblioteki niezgodne z Edge Runtime

Wiele popularnych bibliotek npm nie jest kompatybilnych ze środowiskiem wykonawczym Vercel Edge: Prisma (używa sterowników natywnych), bcrypt (powiązanie natywne), Sharp (obrazy natywne). Przed użyciem biblioteki w oprogramowaniu pośrednim sprawdź, czy obsługuje ona środowisko wykonawcze brzegowe. Prisma ma adapter kompatybilny z krawędziami (@prisma/adapter-pg con @neondatabase/serverless), ale nie wszystkie funkcje są dostępne.

Debugowanie i testowanie oprogramowania pośredniego

Oprogramowanie pośrednie można testować lokalnie za pomocą next dev. Niektóre ograniczenia: request.geo zawsze tak jest undefined lokalnie (użyj próby), a Edge Config wymaga zmiennej środowiskowej 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;
}

Wnioski i dalsze kroki

Oprogramowanie pośrednie Vercel jest jednym z najpotężniejszych narzędzi do dostosuj zachowanie aplikacji Next.js bez modyfikacji logika po stronie serwera. Jego położenie na krawędzi — przed renderowaniem i pamięć podręczna — sprawia, że idealnie nadaje się do szybkich decyzji dotyczących routingu nie wymagają dostępu do bazy danych.

Pokazane wzorce (gelokalizacja, testy A/B, flagi funkcji, uwierzytelnianie) obejmują większość typowych przypadków użycia. Kluczem do sukcesu jest zachowaj lekkość oprogramowania pośredniego: złożona logika lub powolny dostęp do bazy danych należą do Komponentów Serwera lub Procedur Obsługi Tras.

Następne artykuły z serii

  • Artykuł 7: Trasowanie geograficzne na krawędzi — personalizacja Zgodność z treścią i RODO: geofencing z pracownikami Cloudflare, zlokalizowane ceny i zablokować zgodnie z lokalnymi przepisami.
  • Artykuł 8: Cache API i strategie unieważniania w Cloudflare Pracownicy: nieaktualne podczas ponownej walidacji i unieważnianie według klucza w produkcji.
  • Artykuł 9: Testowanie pracowników lokalnych — Miniflare, Vitest i Wrangler Dev: kompletny przepływ pracy do testowania bez wdrażania.