Ir al contenido

Next.js

Los frameworks SSR como Next.js hacen llamadas fetch del lado del servidor que pasan por el proxy sin un contexto de navegador. El proxy identifica a qué sesión pertenecen esas peticiones mediante la cabecera x-test-rcrd-id. El playwrightProxy.before() de Playwright ya la establece en la navegación del navegador que dispara el SSR, así que el id está disponible en next/headers — el trabajo es adjuntarlo a las peticiones salientes del lado del servidor. (Las pruebas solo de navegador no necesitan nada de esto; el proxy recurre a la sesión establecida globalmente.)

Una línea en tu root layout etiqueta cada fetch del lado del servidor — Server Components, Route Handlers, en los runtimes Node y Edge:

app/layout.tsx
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch(); // no-op en producción salvo TEST_PROXY_RECORDER_ENABLED=true

Parchea el fetch global para copiar el x-test-rcrd-id de la petición actual en las peticiones salientes, de modo que el proxy pueda distinguir sesiones de reproducción concurrentes. Llámalo desde el root layout — no desde instrumentation.ts, cuyo contexto difiere del que renderiza tus rutas en el runtime Edge, así que un parche allí nunca se dispara de forma silenciosa.

Si tus peticiones del lado del servidor van por axios, registra cada instancia del lado del servidor una vez:

import { registerProxyAxios } from 'test-proxy-recorder/nextjs';
registerProxyAxios(axiosForServer);

Añade un interceptor de petición que estampa el id (sin tocar el fetch global), así que es inmune a la advertencia del servidor de desarrollo de arriba. No-op en producción / en el navegador; idempotente por instancia; nunca sobrescribe un id establecido por el llamador.

Por llamada — createHeadersWithRecordingId

Sección titulada «Por llamada — createHeadersWithRecordingId»

Sin parchear, y funciona también bajo next dev. Úsalo para un único fetch, o cuando prefieras no parchear el fetch global:

import { headers } from 'next/headers';
import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
const res = await fetch('http://localhost:8100/api/data', {
headers: createHeadersWithRecordingId(await headers(), {
'Content-Type': 'application/json',
}),
});

Un proxy.ts (Next.js 16+, función exportada proxy) o middleware.ts (15 y anteriores, función exportada middleware) que llame a setNextProxyHeaders hace el id disponible vía next/headers, pero no etiqueta los fetch salientes — así que no es necesario si usas uno de los helpers de arriba. Recurre a él solo si ya tienes un middleware (auth, etc.), y aún así combínalo con un helper para hacer el etiquetado:

// proxy.ts (Next.js 16+)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
export function proxy(request: NextRequest) {
const response = NextResponse.next();
setNextProxyHeaders(request, response); // expone el id; combínalo con un helper de arriba
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Mira la referencia de la API para las firmas completas de los helpers de test-proxy-recorder/nextjs. Un proyecto Edge completo y ejecutable vive en el ejemplo de runtime Edge.

No deshabilites el cacheo para las pruebas — el grabador funciona con una ruta cacheada/ISR. Pero hay una regla que define todo el diseño: para reproducir un fetch SSR, la página debe ejecutar ese fetch en el momento de la petición. Una ruta que sirve HTML prerenderizado o un render cacheado obsoleto nunca hace el fetch, así que el proxy no tiene nada que servir y la aserción ve contenido obsoleto.

La forma que se mantiene determinista es cachear el fetch SSR con next.revalidate + next.tags a nivel de fetch, y luego invalidar bajo demanda antes de la aserción:

// app/isr/page.tsx — sin `export const dynamic`, sin `export const revalidate`
const res = await fetch(`${BACKEND_URL}/todos`, {
next: { revalidate: 30, tags: ['isr-todos'] },
});
app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
revalidateTag('isr-todos', 'max'); // Next.js 16 requiere el 2º argumento de perfil
e2e/isr.spec.ts
await page.request.post('/api/revalidate'); // purga dura
await page.goto('/isr'); // una navegación — determinista
await expect(page.getByTestId('todo-text')).toHaveCount(1);

revalidateTag sobre una entrada de caché de fetch es una purga dura: la siguiente lectura es un fallo de caché que se bloquea y vuelve a hacer el fetch a través del proxy. Debes purgar antes de la navegación de reproducción porque la caché de datos sobrevive entre las fases de grabación → reproducción de un mismo proceso next start — de lo contrario la reproducción sirve la caché de la fase de grabación y nunca llega al proxy (un falso positivo).

Durante las pruebas el fetch parcheado lee headers(), así que la página se renderiza dinámicamente y realmente ejecuta el fetch. En producción (grabador deshabilitado) nada lee headers() y la página es ISR estática como siempre — el render dinámico está acotado a las pruebas, y es intrínseco a grabar un fetch SSR.

La revalidación bajo demanda es privilegiada (purga la caché y fuerza la regeneración), así que protege la ruta con un secreto compartido — falla en cerrado si no está definido, compara en tiempo constante, y adjunta el token desde la prueba vía use.extraHTTPHeaders de Playwright para que el spec nunca lo maneje.

Mira el ejemplo completo y ejecutable (parte del ejemplo de Next.js 16):

Inicia los servicios desde scripts, no desde playwright.config.ts:

{
"scripts": {
"mock": "node mock-backend/server.mjs",
"proxy": "test-proxy-recorder http://localhost:3002 -p 8100 -d ./e2e/recordings",
"start:all": "concurrently \"pnpm mock\" \"pnpm proxy\" \"pnpm build && next start --port 3000\""
}
}

Un proyecto completo y ejecutable vive en el ejemplo de Next.js 16.