Article · Network & API Mocking for Reliable Tests

Isolating APIRequestContext in Playwright Tests

Playwright's request fixture and the browser page share state more often than teams expect, and a leaked cookie or auth token bleeding between an APIRequestContext and the browser context is a classic source of order-dependent flakiness. This guide shows how to isolate API contexts with their own storageState, dispose them deterministically, and keep API-driven setup from polluting UI assertions. It extends the patterns in Playwright Route Mocking Strategies by focusing on the request layer rather than page-level interception.

10 sections URL: /network-api-mocking-for-reliable-tests/playwright-route-mocking-strategies/isolating-apirequestcontext-in-playwright-tests/
Shared versus isolated request contexts Two test flows: one where a shared cookie jar leaks auth between API and browser contexts, and one where each context owns its own storageState. Shared context (leaky) API context Browser context shared cookie jar Isolated contexts (stable) API ctx + state A Browser ctx + state B no shared state dispose() after each test frees sockets + cookies no bleed into next spec
Sharing a cookie jar leaks auth across contexts; isolated contexts with their own storageState and explicit disposal stay deterministic.

Root cause #

APIRequestContext is the object behind both playwright.request and the per-test request fixture. It maintains its own cookie jar, headers, and connection pool. When you create it from the same browser context (via context.request), it shares that context’s cookies. A login performed through the API fixture writes a session cookie that the browser page then reuses — and vice versa. In one test that is convenient; across a suite it becomes nondeterministic. Test B passes only because test A happened to authenticate first, and the failure surfaces the moment the runner reorders, shards, or retries specs.

The async execution model amplifies this. Contexts hold live sockets. If a context is never disposed, its keep-alive connections and pending cookies survive into the next test through the worker’s reused process. On CI, where workers run many specs back to back, a single undisposed context can silently authorize requests that should have failed with a 401, masking real auth regressions.

Step-by-step fix #

1. Create a dedicated API context with its own storageState #

Do not reach for context.request. Build a standalone context from the playwright fixture so it owns its cookie jar.

// tests/fixtures/api.js
const { test: base } = require('@playwright/test');

exports.test = base.extend({
  apiContext: async ({ playwright }, use) => {
    // Fresh storageState ({}) means zero inherited cookies/auth — full isolation.
    const ctx = await playwright.request.newContext({
      baseURL: process.env.API_URL,
      storageState: { cookies: [], origins: [] }
    });
    await use(ctx);
    // Trade-off: disposing every test costs a few ms but prevents socket/cookie bleed.
    await ctx.dispose();
  }
});

2. Authenticate the API context separately from the browser #

Persist the API session to its own file so it never overwrites the UI session.

// global-setup.js
const { request } = require('@playwright/test');

module.exports = async () => {
  const api = await request.newContext({ baseURL: process.env.API_URL });
  await api.post('/login', { data: { user: 'svc', pass: process.env.PW } });
  // Write to a distinct path so browser storageState and API storageState never collide.
  await api.storageState({ path: 'storage/api-state.json' });
  await api.dispose();
};

3. Wire isolated states into the project config #

Give the UI project and any API-driven setup distinct state files.

// playwright.config.js
module.exports = {
  globalSetup: require.resolve('./global-setup.js'),
  use: {
    // Browser pages load only the UI session — the API session lives elsewhere.
    storageState: 'storage/ui-state.json'
  }
};

4. Dispose explicitly when you create contexts ad hoc #

Inside a test that spins up a one-off context for seeding, always dispose it.

test('seed then assert UI', async ({ playwright, page }) => {
  const seed = await playwright.request.newContext({ baseURL: process.env.API_URL });
  await seed.post('/orders', { data: { sku: 'A1' } });
  // Dispose before the UI assertion so the seed context can't leak cookies into `page`.
  await seed.dispose();
  await page.goto('/orders');
  await expect(page.getByText('A1')).toBeVisible();
});

Pitfalls #

  • Using context.request for API calls — it shares the browser cookie jar. Mitigation: create standalone contexts with playwright.request.newContext().
  • Forgetting dispose() — leaks sockets and cookies into later specs. Mitigation: dispose in the fixture teardown so it always runs, even on failure.
  • One shared storageState file for both UI and API — auth from one overwrites the other. Mitigation: use distinct paths per session type.
  • Relying on test order for auth — passes locally, fails when sharded. Mitigation: each test authenticates its own context or loads an explicit state file.
  • Reusing a module-level context across files — state survives between specs. Mitigation: scope context creation to the fixture, not the module.

Reliability targets #

Target Goal
Order-dependent flakiness < 0.5% of runs
Context disposal coverage 100% of created contexts
Auth-bleed incidents per 1k runs 0
CI pass rate (sharded) ≥ 99.5%

Frequently Asked Questions #

Q: Does the request fixture share cookies with page by default? A: Only if you derive it from the same browser context. The top-level request fixture and playwright.request.newContext() are independent unless you explicitly load the same storageState.

Q: When should I call dispose() versus letting Playwright clean up? A: Playwright disposes fixture-scoped contexts automatically, but any context you create manually inside a test must be disposed by you to free its sockets and cookies before the next test runs.

Q: Can I reuse one API context across a whole file for speed? A: You can, but it reintroduces cross-test state. Prefer per-test isolation; if performance matters, share read-only contexts only and never ones that mutate auth state.