How to fix Playwright APIRequestContext cookie mismatch between API and UI tests?

Playwright

When Playwright's request fixture (or APIRequestContext) is used alongside page in the same test, their cookies and session state are not automatically synchronized. The request fixture runs in its own isolated context by default — it doesn't share the browser context's cookies even if the user is logged in via the page. This causes API calls to return 401 or redirect to login while the UI shows the authenticated user correctly. Sharing storageState across both contexts is the fix.

Common mistake

test('creates project via API then verifies in UI', async ({ page, request }) => {
  // UI login works fine
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('**/dashboard');

  // But request context has no cookies — returns 401
  const response = await request.post('/api/projects', {
    data: { name: 'New Project' },
  });
  expect(response.ok()).toBeTruthy(); // Fails — unauthenticated
});

The fix

Share authentication by saving storageState from the authenticated browser context and loading it into the APIRequestContext:

import { test, expect } from '@playwright/test';
import path from 'path';

const AUTH_FILE = path.join(__dirname, '../playwright/.auth/user.json');

// In your setup project, generate the auth file once:
// await page.context().storageState({ path: AUTH_FILE });

test('creates project via API then verifies in UI', async ({ page, playwright }) => {
  // Create an APIRequestContext that shares the auth state
  const requestContext = await playwright.request.newContext({
    baseURL: 'https://app.example.com',
    storageState: AUTH_FILE,
  });

  // API request is now authenticated
  const createResponse = await requestContext.post('/api/projects', {
    data: { name: 'Integration Test Project', slug: 'itp-001' },
  });
  expect(createResponse.ok()).toBeTruthy();
  const project = await createResponse.json();

  // UI navigates to the created project
  await page.goto(`/projects/${project.id}`);
  await expect(page.getByRole('heading', { name: 'Integration Test Project' })).toBeVisible();

  await requestContext.dispose();
});

Alternatively, use the request fixture configured to share the project's storageState via playwright.config.ts:

// playwright.config.ts
export default defineConfig({
  use: {
    storageState: 'playwright/.auth/user.json',
    baseURL: 'https://app.example.com',
  },
});
// When storageState is configured globally, the request fixture also uses it
test('API request uses shared auth', async ({ request }) => {
  const response = await request.get('/api/user/me');
  expect(response.ok()).toBeTruthy();

  const user = await response.json();
  expect(user.email).toBe('user@example.com');
});

Why it works

The request fixture in Playwright creates an APIRequestContext that is independent from the browser's BrowserContext. When storageState is specified in use configuration or passed directly to playwright.request.newContext(), Playwright pre-loads the cookies from the saved state file into the request context's cookie jar. This gives the HTTP client the same session cookies the browser has, so API calls succeed with the same authentication. The playwright apirequestcontext storagestate pattern is the canonical way to keep browser and API test sessions in sync.

Tips

  • After making mutations via APIRequestContext, call await requestContext.storageState() and compare the cookie set if you need to verify no session-invalidating side effects occurred.
  • The request fixture (lowercase) in tests uses the project's configured storageState automatically — only create a separate APIRequestContext when you need different auth or a different base URL.
  • For testing unauthenticated API behavior, create a separate APIRequestContext without storageState rather than relying on the test's potentially-authenticated request fixture.
  • Dispose of manually created request contexts with await requestContext.dispose() in test teardown to avoid connection leaks.