Next.js
SSR-фреймворки вроде Next.js делают серверные вызовы fetch, которые проходят через прокси без контекста браузера. Прокси определяет, какой сессии принадлежат эти запросы, по заголовку x-test-rcrd-id. playwrightProxy.before() Playwright уже устанавливает его на навигацию браузера, которая трегерит SSR, поэтому id доступен в next/headers — задача в том, чтобы прикрепить его к исходящим серверным запросам. (Тестам только в браузере всё это не нужно; прокси откатывается к глобально заданной сессии.)
registerProxyFetch (рекомендуется)
Заголовок раздела «registerProxyFetch (рекомендуется)»Одна строка в вашем root layout тегирует каждый серверный fetch — Server Components, Route Handlers, на Node и Edge-runtime:
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch(); // no-op in production unless TEST_PROXY_RECORDER_ENABLED=trueОн патчит глобальный fetch, чтобы копировать x-test-rcrd-id текущего запроса на исходящие запросы, чтобы прокси мог различать конкурентные сессии воспроизведения. Вызывайте его из root layout — не из instrumentation.ts, чей контекст отличается от того, который рендерит ваши роуты на Edge-runtime, поэтому патч там тихо никогда не срабатывает.
axios — registerProxyAxios
Заголовок раздела «axios — registerProxyAxios»Если ваши серверные запросы идут через axios, зарегистрируйте каждый серверный инстанс один раз:
import { registerProxyAxios } from 'test-proxy-recorder/nextjs';
registerProxyAxios(axiosForServer);Он добавляет request-перехватчик, который штампует id (не трогая глобальный fetch), поэтому он неуязвим к приведённой выше dev-серверной проблеме. No-op в продакшене / в браузере; идемпотентен на инстанс; никогда не перезаписывает id, заданный вызывающим.
На каждый вызов — createHeadersWithRecordingId
Заголовок раздела «На каждый вызов — createHeadersWithRecordingId»Без патча, и работает под next dev тоже. Используйте для одиночного fetch или когда не хочется патчить глобальный fetch:
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', }),});Middleware (опционален)
Заголовок раздела «Middleware (опционален)»proxy.ts (Next.js 16+, экспортируемый proxy) или middleware.ts (15 и ранее, экспортируемый middleware) с вызовом setNextProxyHeaders делает id доступным через next/headers, но не тегирует исходящие fetch — поэтому он не требуется, когда вы используете один из хелперов выше. Обращайтесь к нему только если у вас уже есть middleware (auth и пр.), и всё равно комбинируйте его с хелпером для тегирования:
// 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); // exposes the id; pair with a helper above return response;}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],};Полные сигнатуры хелперов test-proxy-recorder/nextjs см. в справочнике API. Полный, готовый к запуску Edge-проект находится в примере Edge-runtime.
Кеширование и ISR
Заголовок раздела «Кеширование и ISR»Не отключайте кеширование ради тестов — рекордер работает с кешируемым/ISR-роутом. Но есть одно правило, определяющее весь дизайн: чтобы воспроизвести SSR-fetch, страница должна выполнить этот fetch в момент запроса. Роут, отдающий пререндеренный HTML или устаревший закешированный рендер, fetch не делает, поэтому прокси нечего отдавать, и проверка видит устаревший контент.
Детерминированным остаётся такой подход: кешировать SSR-fetch на уровне fetch через next.revalidate + next.tags, а затем инвалидировать по требованию перед проверкой:
// app/isr/page.tsx — без `export const dynamic`, без `export const revalidate`const res = await fetch(`${BACKEND_URL}/todos`, { next: { revalidate: 30, tags: ['isr-todos'] },});import { revalidateTag } from 'next/cache';revalidateTag('isr-todos', 'max'); // Next.js 16 требует 2-й аргумент-профильawait page.request.post('/api/revalidate'); // жёсткая очисткаawait page.goto('/isr'); // одна навигация — детерминированноawait expect(page.getByTestId('todo-text')).toHaveCount(1);revalidateTag для записи кеша fetch — это жёсткая очистка: следующее чтение становится промахом кеша, который блокируется и заново делает fetch через прокси. Очищать нужно до навигации воспроизведения, потому что кеш данных переживает фазы запись → воспроизведение одного процесса next start — иначе воспроизведение отдаст кеш фазы записи и никогда не попадёт в прокси (ложный успех).
Во время тестов пропатченный fetch читает headers(), поэтому страница рендерится динамически и реально выполняет fetch. В продакшене (рекордер выключен) headers() никто не читает, и страница остаётся статической ISR как обычно — динамический рендер ограничен тестами и является неотъемлемой частью записи SSR-fetch.
Ревалидация по требованию привилегированна (очищает кеш и форсирует регенерацию), поэтому защитите роут общим секретом — отказывайте по умолчанию, если он не задан, сравнивайте за константное время и прикрепляйте токен из теста через use.extraHTTPHeaders Playwright, чтобы spec никогда не работал с секретом напрямую.
Смотрите полный, готовый к запуску пример (часть примера Next.js 16):
app/isr/page.tsx— кешируемая страница (next.tagsна уровне fetch)app/api/revalidate/route.ts— как защититьrevalidateTag: отказ по умолчанию + сравнение секрета за константное времяe2e/isr.spec.ts— инвалидируем, затем одна навигация; проверяем, что вызов ревалидации успешенplaywright.config.ts— загружает.envи прикрепляет секрет черезextraHTTPHeaders
Скрипты package.json
Заголовок раздела «Скрипты package.json»Запускайте сервисы из скриптов, а не из 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\"" }}Полный, готовый к запуску проект находится в примере Next.js 16.