Introduzione: Dal Middleware al Proxy — Cosa Cambia in Next.js 16
Se avete seguito questa serie, sapete già come Next.js gestisce la lettura dei dati con lo streaming e il Partial Prerendering, e come gestisce la scrittura con le Server Actions. Ma c'è un terzo pilastro fondamentale che tiene insieme tutto: il Middleware — o meglio, come lo chiama Next.js 16, il Proxy.
Parliamoci chiaro: il Middleware è il primo codice che viene eseguito quando una richiesta arriva alla vostra applicazione. Prima del routing, prima del rendering, prima di qualsiasi fetch di dati. Intercetta la richiesta e decide cosa farne. Redirect? Rewrite? Aggiungere un header? Bloccare l'accesso? Tutto avviene qui, in un unico file, con una latenza quasi nulla.
Con Next.js 16 arriva un cambiamento importante: il file middleware.ts viene rinominato in proxy.ts. Non è solo un cambio cosmetico — è un segnale chiaro sulla direzione del framework. Il termine "middleware" generava confusione con il middleware di Express.js e, onestamente, incoraggiava un uso eccessivo della funzionalità. Il nome "Proxy" riflette molto meglio cosa fa effettivamente questo livello: si piazza a livello di rete davanti alla vostra app, esattamente come un proxy.
Ma il cambiamento più significativo non è il nome. È il runtime.
Il nuovo proxy.ts gira su Node.js, non più sull'Edge Runtime. Accesso completo alle API di Node.js, niente più limitazioni su file system, librerie native o moduli CommonJS. Il vecchio middleware.ts resta disponibile per chi ha bisogno dell'Edge Runtime, ma è ufficialmente deprecato e verrà rimosso in una versione futura.
In questa guida vedremo tutto: dalla migrazione al nuovo Proxy, ai pattern di autenticazione, dalla protezione delle route alla lezione di sicurezza lasciata dalla vulnerabilità CVE-2025-29927. Ogni esempio è aggiornato a Next.js 16 e pronto per la produzione.
Come Funziona il Proxy: Anatomia e Ciclo di Vita della Richiesta
Quando un utente fa una richiesta alla vostra app Next.js, il Proxy è il primo strato di logica che viene eseguito. Ecco l'ordine preciso:
- Headers configurati in
next.config.js— applicati per primi - Redirects configurati in
next.config.js - Proxy (
proxy.ts) — la vostra logica personalizzata - Rewrites configurati in
next.config.js(beforeFiles) - Route matching — il framework trova la pagina corrispondente
- Rendering — Server Component, Client Component, o cache
Il punto chiave? Il Proxy viene eseguito prima del contenuto nella cache e del route matching. Per questo è il posto ideale per operazioni a livello di richiesta: controlli di autenticazione, logging, A/B testing e routing basato sulla geolocalizzazione.
Il File proxy.ts: Struttura Base
Il file proxy.ts (o proxy.js) va posizionato nella root del progetto o dentro la cartella src. Può avere un'esportazione di default o una named export chiamata proxy:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
// La vostra logica qui
return NextResponse.next()
}
// Opzionale: configurazione del matcher
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
Alcune regole fondamentali da tenere a mente:
- Potete avere un solo file proxy per progetto
- Il Proxy gira su Node.js runtime — accesso completo alle API di Node
- Senza
matcher, il Proxy viene eseguito per ogni route — usate sempre i matcher per evitare problemi di performance - Il Proxy deve sempre restituire una risposta — anche se non fate nulla, restituite
NextResponse.next()
NextRequest e NextResponse: Le API Fondamentali
NextRequest estende la Web API Request con proprietà specifiche di Next.js: cookies, headers, URL, parametri della query e dati di geolocalizzazione. NextResponse vi permette di modificare la risposta — redirect, rewrite, aggiungere headers, impostare cookies.
Vediamo un esempio completo con le API principali:
// proxy.ts — Esempio completo con le API principali
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
// Leggere un cookie
const token = request.cookies.get('session-token')?.value
// Leggere un header
const acceptLanguage = request.headers.get('accept-language')
// Accedere all'URL e ai parametri
const { pathname, searchParams } = request.nextUrl
const ref = searchParams.get('ref')
// Geolocalizzazione (disponibile su Vercel)
const country = request.geo?.country || 'IT'
// Modificare gli headers della richiesta
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-country', country)
requestHeaders.set('x-pathname', pathname)
// Passare le modifiche alla route
const response = NextResponse.next({
request: { headers: requestHeaders },
})
// Aggiungere un header alla risposta
response.headers.set('x-proxy-version', '1.0')
return response
}
Matcher: Controllare su Quali Route Gira il Proxy
Il matcher è, a mio parere, la parte più importante della configurazione del Proxy. Senza di esso, il vostro codice viene eseguito per ogni singola richiesta — incluse immagini, font, file statici e favicon. Non solo è inutile, ma può causare seri problemi di performance.
Pattern di Base
// Un singolo percorso
export const config = {
matcher: '/dashboard',
}
// Più percorsi
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/api/protected/:path*'],
}
// Regex: escludere file statici e immagini
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon\\.ico|sitemap\\.xml|robots\\.txt).*)',
],
}
Matcher Avanzato con Oggetti
Il matcher accetta anche un array di oggetti con chiavi specifiche per un controllo ancora più granulare:
export const config = {
matcher: [
{
source: '/api/:path*',
has: [
{ type: 'header', key: 'authorization' },
],
},
{
source: '/dashboard/:path*',
missing: [
{ type: 'cookie', key: 'session-token' },
],
},
],
}
La proprietà has specifica condizioni che devono essere presenti (headers, cookie, query parameters), mentre missing specifica condizioni che non devono esserci. In pratica, il Proxy scatta solo quando effettivamente serve.
Un dettaglio che vale la pena sottolineare: anche quando escludete _next/data dal matcher con un pattern negativo, il Proxy verrà comunque invocato per le route _next/data. È intenzionale — impedisce che proteggiate una pagina dimenticandovi della corrispondente route dei dati.
Migrazione da middleware.ts a proxy.ts in Next.js 16
La migrazione è relativamente indolore, ma ci sono alcune differenze critiche. Vediamo passo per passo.
Passo 1: Rinominare il File
Rinominate middleware.ts (o middleware.js) in proxy.ts (o proxy.js). Semplice.
Passo 2: Rinominare la Funzione Esportata
// PRIMA: middleware.ts
export function middleware(request: NextRequest) {
// ...
}
// DOPO: proxy.ts
export function proxy(request: NextRequest) {
// ...
}
Passo 3: Aggiornare i Flag di Configurazione
Se usavate flag speciali nel next.config.js, aggiornateli:
// PRIMA
const nextConfig = {
skipMiddlewareUrlNormalize: true,
}
// DOPO
const nextConfig = {
skipProxyUrlNormalize: true,
}
Passo 4: Usare il Codemod Automatico
Se preferite non fare tutto a mano, Next.js fornisce un codemod che automatizza la migrazione — rinomina il file e la funzione in un colpo solo:
npx @next/codemod@latest middleware-to-proxy .
Attenzione al Cambio di Runtime
Questa è la differenza che conta davvero. Il vecchio middleware.ts girava sull'Edge Runtime — un runtime JavaScript leggero con cold start quasi nulli, distribuito globalmente sulle CDN. Però aveva limitazioni significative: niente API Node.js native, niente file system, niente moduli CommonJS, limite di 1MB sul bundle.
Il nuovo proxy.ts gira su Node.js. Cosa significa in pratica?
- Pro: accesso completo alle API di Node.js, nessun limite di bundle, possibilità di usare qualsiasi libreria npm
- Contro: non viene distribuito globalmente sull'edge, quindi la latenza potrebbe essere leggermente maggiore per utenti distanti dal server di origine
Se il vostro caso d'uso richiede specificamente l'Edge Runtime (ad esempio, geolocalizzazione a latenza ultra-bassa), potete continuare a usare middleware.ts per ora — ma tenete presente che è deprecato e il suo tempo è contato.
Pattern di Autenticazione: Proteggere le Route con il Proxy
Ok, arriviamo al cuore della questione. L'autenticazione è il caso d'uso principale del Proxy, e ci sono diversi approcci. Vediamoli dal più semplice al più robusto.
Pattern 1: Verifica JWT con jose
Il pattern più diretto: verificare un JSON Web Token contenuto in un cookie. La libreria jose è la scelta migliore perché è leggera e compatibile sia con Node.js che con l'Edge Runtime (utile se state ancora usando middleware.ts):
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)
const PROTECTED_ROUTES = ['/dashboard', '/account', '/settings']
const AUTH_ROUTES = ['/login', '/register']
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
const token = request.cookies.get('auth-token')?.value
const isProtectedRoute = PROTECTED_ROUTES.some(route =>
pathname.startsWith(route)
)
const isAuthRoute = AUTH_ROUTES.some(route =>
pathname.startsWith(route)
)
if (isProtectedRoute) {
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
// Passare i dati dell'utente alla route tramite header
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', payload.sub as string)
requestHeaders.set('x-user-role', payload.role as string)
return NextResponse.next({
request: { headers: requestHeaders },
})
} catch {
// Token invalido o scaduto
const response = NextResponse.redirect(
new URL('/login', request.url)
)
response.cookies.delete('auth-token')
return response
}
}
// Utenti autenticati non dovrebbero vedere login/register
if (isAuthRoute && token) {
try {
await jwtVerify(token, JWT_SECRET)
return NextResponse.redirect(new URL('/dashboard', request.url))
} catch {
// Token invalido, lascia accedere alla pagina di login
}
}
return NextResponse.next()
}
export const config = {
matcher: [
'/dashboard/:path*',
'/account/:path*',
'/settings/:path*',
'/login',
'/register',
],
}
Pattern 2: Controllo degli Accessi Basato sui Ruoli (RBAC)
Per applicazioni con permessi granulari, potete estendere la verifica JWT con un sistema di ruoli. Nella mia esperienza, questo è un pattern che prima o poi serve a quasi tutti i progetti di una certa dimensione:
// lib/rbac.ts
type Role = 'user' | 'editor' | 'admin'
const ROUTE_PERMISSIONS: Record<string, Role[]> = {
'/dashboard': ['user', 'editor', 'admin'],
'/dashboard/analytics': ['editor', 'admin'],
'/admin': ['admin'],
'/admin/users': ['admin'],
}
export function hasAccess(pathname: string, userRole: Role): boolean {
// Trova la regola più specifica che corrisponde al percorso
const matchingRoutes = Object.entries(ROUTE_PERMISSIONS)
.filter(([route]) => pathname.startsWith(route))
.sort(([a], [b]) => b.length - a.length) // Più specifica prima
if (matchingRoutes.length === 0) return true // Nessuna restrizione
const [, allowedRoles] = matchingRoutes[0]
return allowedRoles.includes(userRole)
}
// proxy.ts — sezione RBAC
import { hasAccess } from './lib/rbac'
// Dentro la funzione proxy, dopo la verifica JWT:
const userRole = payload.role as Role
if (!hasAccess(pathname, userRole)) {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
Pattern 3: Integrazione con Auth.js (NextAuth v5)
Se usate Auth.js, le cose si semplificano parecchio. Auth.js fornisce un helper che si integra direttamente con il sistema di sessione:
// auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
export const { auth, handlers, signIn, signOut } = NextAuth({
providers: [GitHub],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard')
if (isOnDashboard) {
return isLoggedIn // Redirect non autenticati a /login
}
return true
},
},
})
// proxy.ts
export { auth as proxy } from './auth'
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
}
L'eleganza di questo approccio è notevole: la logica di autorizzazione vive nel callback authorized, e Auth.js si occupa di tutto il resto — verifica della sessione, gestione dei cookie, serializzazione dei dati utente.
La Lezione di CVE-2025-29927: Mai Fidarsi Solo del Proxy
Nel marzo 2025 è stata divulgata una delle vulnerabilità più critiche nella storia di Next.js: CVE-2025-29927, con un punteggio CVSS di 9.1 su 10. E onestamente, la lezione che ci ha lasciato è più importante di qualsiasi pattern di codice che vedremo in questa guida.
Cosa è Successo
Il Middleware di Next.js usava un header HTTP interno chiamato x-middleware-subrequest per evitare che le richieste venissero elaborate più volte in loop infiniti. Il problema? Questo header poteva essere inviato da chiunque dall'esterno.
Un attaccante che aggiungeva questo header alla propria richiesta poteva far saltare completamente l'esecuzione del Middleware — e con esso, tutti i controlli di autenticazione e autorizzazione. Pensateci un attimo: bastava un singolo header per bypassare tutta la sicurezza.
Tutte le versioni di Next.js prima della 12.3.5, 13.5.9, 14.2.25 e 15.2.3 erano vulnerabili. Le applicazioni deployate su Vercel erano automaticamente protette, ma quelle self-hosted erano completamente esposte.
Il Principio della Defense-in-Depth
La vera lezione di CVE-2025-29927 non è "aggiornate Next.js" (anche se, ovviamente, fatelo subito). La lezione è: non affidatevi mai a un singolo punto di controllo per la sicurezza. Il Proxy deve essere la prima linea di difesa, non l'unica.
L'approccio corretto è la defense-in-depth — verificare l'autenticazione a ogni livello:
// 1. LIVELLO PROXY: prima linea di difesa
// proxy.ts — verifica il token JWT (come visto sopra)
// 2. LIVELLO DATA ACCESS LAYER: seconda linea di difesa
// lib/dal.ts
import { cookies } from 'next/headers'
import { cache } from 'react'
import { jwtVerify } from 'jose'
export const getCurrentUser = cache(async () => {
const cookieStore = await cookies()
const token = cookieStore.get('auth-token')?.value
if (!token) return null
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET!)
)
return {
id: payload.sub as string,
role: payload.role as string,
email: payload.email as string,
}
} catch {
return null
}
})
// 3. LIVELLO SERVER COMPONENT: verifica prima di renderizzare
// app/dashboard/page.tsx
import { getCurrentUser } from '@/lib/dal'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) redirect('/login')
// Solo a questo punto renderizzate i dati sensibili
const dashboardData = await fetchDashboardData(user.id)
return <Dashboard data={dashboardData} />
}
// 4. LIVELLO SERVER ACTION: verifica prima di mutare
// app/actions/settings.ts
'use server'
import { getCurrentUser } from '@/lib/dal'
export async function updateProfile(formData: FormData) {
const user = await getCurrentUser()
if (!user) throw new Error('Non autorizzato')
// Solo ora eseguite la mutazione
await db.user.update({
where: { id: user.id },
data: { name: formData.get('name') as string },
})
}
Notate l'uso di React.cache() nel Data Access Layer: garantisce che getCurrentUser venga eseguita una sola volta per richiesta, anche se chiamata da più componenti. Zero overhead aggiuntivo, ma sicurezza completa.
Pattern Avanzati: Oltre l'Autenticazione
Il Proxy non serve solo per l'autenticazione. Anzi, alcuni degli utilizzi più interessanti sono completamente diversi. Vediamo alcuni pattern avanzati che sfruttano la sua posizione unica nel ciclo di vita della richiesta.
Internazionalizzazione (i18n) con Redirect Automatici
Rilevare la lingua dell'utente e reindirizzarlo alla versione localizzata del sito è un caso d'uso perfetto per il Proxy:
// proxy.ts — i18n routing
import { NextRequest, NextResponse } from 'next/server'
import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
const LOCALES = ['it', 'en', 'de', 'fr', 'es']
const DEFAULT_LOCALE = 'it'
function getPreferredLocale(request: NextRequest): string {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => {
negotiatorHeaders[key] = value
})
const languages = new Negotiator({
headers: negotiatorHeaders,
}).languages(LOCALES)
return matchLocale(languages, LOCALES, DEFAULT_LOCALE)
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// Controlla se il pathname ha già un prefisso locale
const pathnameHasLocale = LOCALES.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return NextResponse.next()
// Determina la locale e redirect
const locale = getPreferredLocale(request)
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon\\.ico).*)' ],
}
A/B Testing con Cookie Persistenti
Un altro pattern che trovo molto utile nei progetti reali: assegnare utenti a varianti di esperimento tramite cookie e servire versioni diverse della stessa pagina. Il bello è che l'utente non vede mai un URL diverso:
// proxy.ts — A/B testing
import { NextRequest, NextResponse } from 'next/server'
const EXPERIMENT_COOKIE = 'ab-homepage-v2'
const VARIANTS = ['control', 'variant-a', 'variant-b'] as const
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname === '/') {
// Controlla se l'utente ha già una variante assegnata
let variant = request.cookies.get(EXPERIMENT_COOKIE)?.value
if (!variant || !VARIANTS.includes(variant as any)) {
// Assegna una variante casuale
variant = VARIANTS[Math.floor(Math.random() * VARIANTS.length)]
}
// Rewrite alla pagina della variante (URL non cambia per l'utente)
const response = NextResponse.rewrite(
new URL(`/experiments/homepage/${variant}`, request.url)
)
// Persisti la variante nel cookie per 30 giorni
response.cookies.set(EXPERIMENT_COOKIE, variant, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: 'lax',
})
return response
}
return NextResponse.next()
}
Rate Limiting Base
Per proteggere gli endpoint API da abusi, potete implementare un rate limiter direttamente nel Proxy. Dato che proxy.ts gira su Node.js, una semplice Map in memoria fa il suo lavoro (attenzione però: funziona solo con un singolo processo — per scenari multi-istanza, vi serve Redis):
// proxy.ts — rate limiting base
import { NextRequest, NextResponse } from 'next/server'
const rateLimitMap = new Map<string, { count: number; lastReset: number }>()
const WINDOW_MS = 60 * 1000 // 1 minuto
const MAX_REQUESTS = 60 // 60 richieste per minuto
function isRateLimited(ip: string): boolean {
const now = Date.now()
const record = rateLimitMap.get(ip)
if (!record || now - record.lastReset > WINDOW_MS) {
rateLimitMap.set(ip, { count: 1, lastReset: now })
return false
}
record.count++
return record.count > MAX_REQUESTS
}
export function proxy(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]
?? request.headers.get('x-real-ip')
?? 'unknown'
if (isRateLimited(ip)) {
return NextResponse.json(
{ error: 'Troppe richieste. Riprova tra un minuto.' },
{ status: 429 }
)
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
Composizione di Più Pattern in un Singolo Proxy
In un'applicazione reale (e se avete lavorato su qualcosa di più grande di un progetto demo, lo sapete bene), dovrete combinare autenticazione, i18n, rate limiting e altri controlli in un unico file proxy. La chiave è organizzare la logica in funzioni separate e comporle in modo pulito:
// proxy.ts — composizione di pattern multipli
import { NextRequest, NextResponse } from 'next/server'
import { verifyAuth } from './lib/auth-proxy'
import { handleI18n } from './lib/i18n-proxy'
import { checkRateLimit } from './lib/rate-limit-proxy'
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Rate limiting sulle API
if (pathname.startsWith('/api')) {
const rateLimitResponse = checkRateLimit(request)
if (rateLimitResponse) return rateLimitResponse
}
// 2. Autenticazione sulle route protette
if (pathname.startsWith('/dashboard') || pathname.startsWith('/account')) {
const authResponse = await verifyAuth(request)
if (authResponse) return authResponse
}
// 3. i18n per le pagine pubbliche
if (!pathname.startsWith('/api') && !pathname.startsWith('/dashboard')) {
const i18nResponse = handleI18n(request)
if (i18nResponse) return i18nResponse
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon\\.ico|sitemap\\.xml|robots\\.txt).*)',
],
}
Errori Comuni e Come Evitarli
Dopo aver visto (e commesso) parecchie implementazioni sbagliate, ecco gli errori più frequenti — e come risolverli.
1. Loop Infiniti di Redirect
L'errore più classico in assoluto: il Proxy reindirizza a /login, ma anche /login è catturata dal matcher, che la reindirizza di nuovo a /login... all'infinito. Il browser vi mostrerà "ERR_TOO_MANY_REDIRECTS" e voi vi chiederete cosa è andato storto.
// ❌ SBAGLIATO: matcher troppo ampio
export const config = {
matcher: '/:path*', // Cattura TUTTO, incluso /login
}
// ✅ CORRETTO: escludere le route di autenticazione
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
}
2. Non Escludere i File Statici
Se il vostro matcher è troppo generico, il Proxy verrà eseguito anche per immagini, CSS e JavaScript. Rallenterete l'intera applicazione senza motivo.
// ✅ Pattern regex che esclude file statici
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon\\.ico|.*\\.png$|.*\\.svg$).*)',
],
}
3. Operazioni Pesanti nel Proxy
Il Proxy viene eseguito per ogni richiesta. Se fate una query al database o chiamate un'API esterna qui dentro, state aggiungendo latenza a ogni singola navigazione. È un errore più comune di quanto si pensi.
// ❌ SBAGLIATO: query al database nel proxy
export async function proxy(request: NextRequest) {
const user = await db.user.findUnique({ where: { id: userId } }) // NO!
// ...
}
// ✅ CORRETTO: verifica solo il JWT (operazione locale)
export async function proxy(request: NextRequest) {
const { payload } = await jwtVerify(token, secret) // Sì — veloce, locale
// ...
}
Regola d'oro: nel Proxy, leggete solo cookie e header. La verifica di un JWT è un'operazione locale e veloce. Le query al database vanno nel Data Access Layer o nei Server Component.
4. Dimenticare NextResponse.next()
Se il Proxy non restituisce nulla per un determinato percorso, la richiesta resta appesa. Restituite sempre una risposta:
// ❌ SBAGLIATO: nessun return nel caso di default
export function proxy(request: NextRequest) {
if (someCondition) {
return NextResponse.redirect(new URL('/login', request.url))
}
// La richiesta resta appesa!
}
// ✅ CORRETTO: return esplicito alla fine
export function proxy(request: NextRequest) {
if (someCondition) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next() // Sempre un return finale
}
5. Esporre Dati Sensibili negli Header
Ricordate che gli header impostati nel Proxy possono essere letti lato client. Non inseriteci mai password, chiavi API o token completi — è un rischio di sicurezza serio:
// ❌ SBAGLIATO: token completo nell'header
requestHeaders.set('x-auth-token', token) // Visibile al client!
// ✅ CORRETTO: solo l'ID utente (dato non sensibile)
requestHeaders.set('x-user-id', payload.sub as string)
Geolocalizzazione e Header Personalizzati su Vercel
Quando deployate su Vercel, il Proxy ha accesso automatico ai dati di geolocalizzazione tramite request.geo. Questo apre scenari interessanti — routing geografico, conformità normativa, personalizzazione dei contenuti:
// proxy.ts — routing geografico
export function proxy(request: NextRequest) {
const country = request.geo?.country
const city = request.geo?.city
const region = request.geo?.region
// Blocco geografico per conformità normativa (es. GDPR)
if (country && ['RU', 'CN'].includes(country)) {
return NextResponse.rewrite(new URL('/geo-blocked', request.url))
}
// Redirect alla versione locale del sito
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(
new URL(`/de${request.nextUrl.pathname}`, request.url)
)
}
// Passare i dati geo ai componenti tramite header
const response = NextResponse.next()
if (country) response.headers.set('x-geo-country', country)
if (city) response.headers.set('x-geo-city', city)
return response
}
Un avvertimento importante: su Vercel, i dati di geolocalizzazione riflettono l'edge node più vicino all'utente, non la posizione esatta. In paesi senza edge node, la geolocalizzazione potrebbe indicare un paese vicino. Se vi serve precisione assoluta, integrate un servizio di geolocalizzazione IP dedicato.
Testing del Proxy
Testare il Proxy è essenziale ma, diciamolo, spesso viene trascurato. A partire da Next.js 15 è disponibile una utility sperimentale per verificare se il Proxy corrisponde a determinati URL:
// __tests__/proxy.test.ts
import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
import { config } from '../proxy'
describe('Proxy matcher', () => {
it('deve intercettare le route della dashboard', () => {
expect(
unstable_doesMiddlewareMatch({
config,
nextConfig: {},
url: '/dashboard/settings',
})
).toBe(true)
})
it('non deve intercettare i file statici', () => {
expect(
unstable_doesMiddlewareMatch({
config,
nextConfig: {},
url: '/_next/static/chunks/main.js',
})
).toBe(false)
})
it('non deve intercettare le route API pubbliche', () => {
expect(
unstable_doesMiddlewareMatch({
config,
nextConfig: {},
url: '/api/health',
})
).toBe(false)
})
})
Per test end-to-end più completi, usate Playwright o Cypress per verificare che i redirect funzionino, che le pagine protette non siano accessibili senza autenticazione e che i cookie vengano impostati come previsto.
Checklist di Sicurezza per il Proxy
Prima di andare in produzione, passate in rassegna ogni punto di questa checklist. Sul serio, non saltatene nemmeno uno:
- Aggiornate Next.js all'ultima versione stabile — le patch di sicurezza sono critiche
- Non affidatevi solo al Proxy per l'autenticazione — implementate la defense-in-depth con un Data Access Layer
- Validate gli input delle Server Actions con Zod o simili — le Server Actions sono endpoint pubblici
- Usate cookie sicuri:
HttpOnly,Secure,SameSite=Lax(oStrict) - Impostate i Content Security Policy tramite header nel Proxy
- Implementate il rate limiting sugli endpoint di autenticazione
- Usate
React.cache()per la memoizzazione delle verifiche di sessione a livello di richiesta - Strippate gli header interni se usate un reverse proxy davanti a Next.js — impedite che header come
x-middleware-subrequestarrivino dall'esterno - Non esponete dati sensibili negli header personalizzati — sono visibili lato client
- Testate i matcher — un matcher errato può lasciare route non protette
Domande Frequenti
Qual è la differenza tra middleware.ts e proxy.ts in Next.js 16?
Funzionalmente fanno la stessa cosa: intercettano le richieste prima del routing. La differenza principale è il runtime: middleware.ts gira sull'Edge Runtime (leggero, distribuito globalmente, ma con API limitate), mentre proxy.ts gira su Node.js (accesso completo alle API, ma eseguito sul server di origine). Il file middleware.ts è deprecato e verrà rimosso in futuro. Per nuovi progetti, usate sempre proxy.ts.
Posso usare middleware.ts e proxy.ts contemporaneamente?
No. Next.js 16 supporta un solo file di intercettazione delle richieste per progetto. Se avete proxy.ts, non potete avere anche middleware.ts — e viceversa. Scegliete in base alle vostre esigenze di runtime.
Il middleware di Next.js è sicuro per l'autenticazione dopo CVE-2025-29927?
La vulnerabilità è stata corretta nelle versioni 12.3.5, 13.5.9, 14.2.25 e 15.2.3. Dopo l'aggiornamento, il Proxy è sicuro come primo livello di difesa. Ma la lezione fondamentale resta: non affidatevi mai esclusivamente al Proxy per la sicurezza. Implementate sempre la verifica dell'autenticazione anche nel Data Access Layer, nei Server Component e nelle Server Actions.
Come faccio a testare il proxy in locale durante lo sviluppo?
Il Proxy viene eseguito automaticamente in modalità sviluppo con next dev. Per testare il comportamento di produzione (incluso ISR e caching), usate next build seguito da next start. Per i test unitari dei matcher, c'è l'utility sperimentale unstable_doesMiddlewareMatch disponibile da Next.js 15.
Il proxy rallenta la mia applicazione?
Dipende interamente da cosa ci mettete dentro. La verifica di un JWT è un'operazione locale che richiede microsecondi. Una query al database o una chiamata API esterna? Centinaia di millisecondi aggiunti a ogni richiesta. Regola d'oro: nel Proxy, leggete solo cookie e header — tutto il resto va nei Server Component o nel Data Access Layer.