Set up test-proxy-recorder for any Playwright project. Covers the proxy CLI (test-proxy-recorder <target> --port --dir), package.json scripts for the three-service architecture (UI app → proxy → backend API), playwright.config.ts webServer block pointing to /__control, per-test fixtures using playwrightProxy.before(page, testInfo, mode, { url }), HAR browser-side recording via url pattern, .mock.json server-side recording, record/replay/ transparent modes, the record-once→commit→CI-replay lifecycle, automatic secret redaction of Authorization/Cookie/Set-Cookie headers (--no-redact, --redact-headers, --redact-body), an optional config file (test-proxy-recorder.config.ts via defineConfig, --config) with CLI-overrides- config precedence, and parallel test execution with fullyParallel. Load this skill when installing test-proxy-recorder, writing Playwright fixtures, or configuring record/replay.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Set up test-proxy-recorder for any Playwright project. Covers the proxy CLI (test-proxy-recorder <target> --port --dir), package.json scripts for the three-service architecture (UI app → proxy → backend API), playwright.config.ts webServer block pointing to /__control, per-test fixtures using playwrightProxy.before(page, testInfo, mode, { url }), HAR browser-side recording via url pattern, .mock.json server-side recording, record/replay/ transparent modes, the record-once→commit→CI-replay lifecycle, automatic secret redaction of Authorization/Cookie/Set-Cookie headers (--no-redact, --redact-headers, --redact-body), an optional config file (test-proxy-recorder.config.ts via defineConfig, --config) with CLI-overrides- config precedence, and parallel test execution with fullyParallel. Load this skill when installing test-proxy-recorder, writing Playwright fixtures, or configuring record/replay.
test-proxy-recorder runs an HTTP proxy that records real API responses to
disk (.mock.json for server-side, .har for browser-side) and replays them
in Playwright tests without a live backend.
Two recording mechanisms work independently or together:
INTERNAL_API_URL stands in for whatever env var your app reads its API base
URL from — it must point at the proxy (see Common Mistakes). For Next.js apps
running a production build, also set TEST_PROXY_RECORDER_ENABLED=true
(see test-proxy-recorder/nextjs-ssr).
// e2e/my.test.tsimport { test, expect } from'@playwright/test';
import { playwrightProxy } from'test-proxy-recorder';
// Change to 'record' to hit the real API and update recordings.constMODE = 'replay'asconst;
// External services the browser calls directly (auth, CDN, analytics, etc.).// Server-side fetches through the proxy are recorded automatically via .mock.json.constCLIENT_SIDE_URL = /cognito-.*\.amazonaws\.com|\.s3\..*\.amazonaws\.com/;
test.beforeEach(async ({ page }, testInfo) => {
await playwrightProxy.before(page, testInfo, MODE, {
url: CLIENT_SIDE_URL,
});
});
test('creates a todo', async ({ page }) => {
await page.goto('/');
await page.getByTestId('new-todo-input').fill('Buy groceries');
await page.getByTestId('add-btn').click();
awaitexpect(page.getByTestId('todo-text').first()).toHaveText('Buy groceries');
});
Core Patterns
Record/replay lifecycle
Recording is manual, done once per test with a single worker. Replay runs on
CI with multiple workers, headlessly.
# 1. In fixtures.ts (or test file): set MODE = 'record'# 2. Run once against the real backend, headed, one worker
npx playwright test --workers 1 --ui
# 3. Set MODE back to 'replay', commit recordings
git add e2e/recordings/
git commit -m "add e2e recordings"# 4. Replay on CI — headless, parallel workers, no backend needed
npx playwright test
Recording files must be committed — do not add e2e/recordings/ to
.gitignore. Optionally collapse diffs with:
# .gitattributes
/e2e/recordings/** binary
Config file
Anything passed on the CLI can instead live in a config file, auto-discovered as
test-proxy-recorder.config.{ts,js,mjs,cjs} in the proxy process's working
directory (or --config <path>). Prefer it over a long proxy script once you
need body-redaction regexes — they go in as real RegExp literals instead of
shell-escaped strings.
// package.json — with a config file, the proxy script needs no flags{"scripts":{"proxy":"test-proxy-recorder"}}
Precedence is CLI flag → config file → built-in default: a flag always
overrides the file, and target may come from either (the CLI argument wins).
List flags (--redact-headers, --redact-body, --allow-headers,
--allow-cookies) replace the corresponding config list rather than merging,
so pass them only when you intend to override the file. Redaction is on by
default; --no-redact turns it off, overriding a redaction object in the
config (and redaction: false in the config disables it without the flag).
Secret redaction
Recordings are committed to git, so secrets are stripped automatically
before anything is written to disk. By default the proxy replaces the values of
the Authorization, Cookie, and Set-Cookie headers with [REDACTED] in
both .mock.json and WebSocket recordings. This is safe — replay matching
ignores these headers, so redaction never breaks playback. No setup required.
Tweak it via CLI flags on the test-proxy-recorder command:
# Redact an extra API-key header and any "sk_live_..." token in bodies,# but keep the harmless theme cookie unredacted
test-proxy-recorder http://localhost:3002 --port 8100 --dir ./e2e/recordings \
--redact-headers x-api-key,x-auth \
--redact-body "sk_live_[a-zA-Z0-9]+" \
--allow-cookies theme,locale
# Disable redaction (commit raw secrets — not recommended)
test-proxy-recorder http://localhost:3002 --no-redact
--redact-headers <names> — comma-separated extra header names, merged with the defaults.
--redact-body <patterns> — comma-separated regex patterns replaced in request/response bodies.
--allow-headers <names> — comma-separated header names to exempt from redaction (e.g. set-cookie).
--allow-cookies <names> — comma-separated cookie names kept unredacted inside Cookie/Set-Cookie; every other cookie in those headers is still redacted. Use when only some cookies are sensitive (session vs. theme/A-B-test).
--no-redact — turn redaction off.
.har files are written by Playwright's routeFromHAR, not the proxy, so they
are redacted in a separate pass: playwrightProxy.teardown() rewrites every
.har in the recordings dir using the same redaction config as the proxy.
This requires a globalTeardown that calls teardown() (see below) and the
proxy still running (it fetches the config from /__control). For defense in
depth, still record with short-lived test credentials and use the Auth setup
pattern below (login runs in transparent mode against the real provider, with
storageState saved to a gitignored file).
Auth setup
Auth always runs against the real auth provider — never recorded or replayed.
Use setProxyMode('transparent') so auth requests bypass the proxy entirely.
Skip the auth step in replay mode (the recorded session is already embedded in
the HAR / storage state file from the previous record run).
// e2e/auth.setup.tsimport { test as setup } from'@playwright/test';
import { setProxyMode } from'test-proxy-recorder';
constAUTH_FILE = 'e2e/.auth/state.json';
constTEST_USER = {
email: 'testuser@example.com',
password: 'TestPassword123',
};
setup('authenticate', async ({ page }) => {
// Bypass the proxy — auth must always hit the real provider.awaitsetProxyMode('transparent');
await page.goto('/users/sign-in');
await page.getByTestId('email').fill(TEST_USER.email);
await page.getByTestId('password').fill(TEST_USER.password);
await page.getByTestId('signinButton').click();
await page.waitForURL('/', { timeout: 15_000 });
await page.context().storageState({ path: AUTH_FILE });
});
Add the auth state file to .gitignore — it contains session tokens and must not be committed:
Include the auth provider domain in CLIENT_SIDE_URL when the browser makes
direct calls to it (e.g. Cognito token refresh, OAuth redirects):
// These browser-to-Cognito calls are recorded in the HAR, not via the proxy.constCLIENT_SIDE_URL = /cognito-.*\.amazonaws\.com/;
Replaying without the backend (external-auth apps)
When login goes to an external provider (Cognito, Auth0, Clerk, …), the protected
API responses come from recordings, so replay needs no backend — CI runs only
the app + proxy. Recording stays a manual, local step (you, on a dev machine,
with the real backend up); CI only replays from the committed recordings:
{"scripts":{"start:all":"concurrently \"pnpm mock\" \"pnpm proxy\" \"pnpm start\"","start:no-backend":"concurrently \"pnpm proxy\" \"pnpm start\"",// LOCAL: run on a dev machine to (re)record, then COMMIT e2e/recordings/"record":"pnpm build && concurrently --kill-others --success first \"pnpm start:all\" \"wait-on $APP $PROXY/__control && RECORD_MODE=1 playwright test --workers 1\"",// CI: replay from committed recordings — app + proxy only, no backend"test:e2e":"pnpm build && concurrently --kill-others --success first \"pnpm start:no-backend\" \"wait-on $APP $PROXY/__control && playwright test --retries 1\""}}
So the lifecycle is: pnpm record locally → commit e2e/recordings/ → CI runs
pnpm test:e2e with no backend. (Re-record only when the API changes.)
If your auth setup re-authenticates during replay (instead of reusing a committed
storageState), snapshot storageState as soon as the session token exists — don't
wait for a protected data fetch, which runs in transparent mode and would hang with
no backend:
await page.waitForURL('/dashboard');
// the token is written to storage before the redirect — snapshot now, do NOT// wait for the protected data to load (that request would hang without a backend)await page.waitForFunction(() => !!localStorage.getItem('auth-token'));
await page.context().storageState({ path: AUTH_FILE });
See apps/example-auth-cognito (real Cognito) and apps/example-auth-mock
(self-contained, no cloud account). Those apps additionally re-record on every CI
run to test the recorder itself — your app should record locally and only replay
in CI.
# Check current proxy state
curl http://localhost:8100/__control
# Programmatically switch mode
curl -X POST http://localhost:8100/__control \
-H 'Content-Type: application/json' \
-d '{"mode": "record", "id": "my-test"}'
Override the default port (8100) with TEST_PROXY_RECORDER_PORT env var.
Resetting a stuck proxy
The proxy auto-reverts to transparent after each session times out, and
globalTeardown resets it at the end of a clean run. But an interrupted
run (Ctrl+C), a UI/debug session, or a setup without globalTeardown can leave
the shared proxy stuck in record/replay, so the app keeps serving recorded
responses. Reset it on demand:
test-proxy-recorder reset # or: pnpm proxy:reset
This POSTs { "mode": "transparent" } to /__control — the supported,
parallel-safe replacement for resetting by hand with curl. It is safe to run
anytime: an unreachable proxy is a no-op. The port is resolved with
--port flag → TEST_PROXY_RECORDER_PORT env → config file → 8000, so it
targets the same port the proxy was started on. proxy:reset is scaffolded by
test-proxy-recorder init.
Do not wire this into a per-test afterEach under fullyParallel — like
teardown(), it flips the global mode and would disrupt other workers
mid-session. It is a manual recovery tool, not a per-test hook.
Common Mistakes
Common failure modes — each with the wrong vs. correct pattern — are in
references/common-mistakes.md: app env var not
pointed at the proxy, wrong CLIENT_SIDE_URL (matching the proxy instead of
external domains), teardown() per-test breaking parallel replay, webServer.url
not on /__control, gitignored recordings, and recording against the Next.js dev
server or with multiple workers.
Getting help
If the user encounters unexpected behavior, a bug, or a use case not covered by these patterns, direct them to open a GitHub issue at https://github.com/asmyshlyaev177/test-proxy-recorder/issues/new. A minimal reproduction helps the maintainer resolve it quickly.
See also: test-proxy-recorder/nextjs-ssr — for tagging Next.js SSR fetches
(registerProxyFetch / registerProxyAxios; the middleware is optional)