In Playwright, how do you make an API request context send calls as the same signed-in role?
answer
- Separate clients, separate cookie jars
- page.request rides the context's cookies
- Seed a new context from the role file
- storageState writes cookies and origins out
- One context per role, never reused
basics
~20 sSeed the API context from that role's saved state file with request.newContext({ storageState }), or issue the calls through page.request, which shares cookies with the page's own context. A sign-in performed in the browser during a test never reaches a separate request context.
solid answer
~40 sAn `APIRequestContext` has its own cookie jar, so an admin session established in the browser is invisible to a separately created one and vice versa. There are two clean ways to make both speak as the same role. Use `page.request` or `context.request`, which reuse that browser context's cookies, so a call issued through them is already the role the page is signed in as. Or build a standalone context from the role's file: `request.newContext({ storageState: 'playwright/.auth/admin.json' })`, which starts with exactly the cookies the capture saved. Going the other way, `apiRequestContext.storageState({ path })` writes the API context's cookies and origins to a file the browser can then load — the basis of signing in over HTTP once and handing the session to the UI tests.
code
typescript · 20 linesimport { test as base, request as apiRequest, expect, type APIRequestContext } from '@playwright/test';
export const test = base.extend<{ adminApi: APIRequestContext }>({
adminApi: async ({}, use) => {
const ctx = await apiRequest.newContext({
baseURL: 'https://console.example.com',
storageState: 'playwright/.auth/admin.json',
});
await use(ctx);
await ctx.dispose();
},
});
test.use({ storageState: 'playwright/.auth/admin.json' });
test('archived staff leave the roster', async ({ page, adminApi }) => {
await adminApi.post('/api/staff/42/archive');
await page.goto('/staff');
await expect(page.getByRole('row', { name: 'Dana Okoye' })).toBeHidden();
});go deeper
Know that API calls and the browser are separate clients with separate cookies, and that page.request is the easy way to call the console as the role the page is signed in as.
Explain both directions: seeding a request context from a role file, and writing a request context's state to a file the browser specs load.
Keep the identity of setup calls deliberate — know when data should be seeded as an admin while the page acts as a viewer, and make that visible in the fixture rather than incidental.
Own the boundary between HTTP setup and UI verification so that a suite's speed does not come at the cost of anyone being able to tell which identity performed which step.
## Two clients, two cookie jars A Playwright test can talk to an admin console two ways: through a page in a browser context, and through an `APIRequestContext` that speaks HTTP directly with no browser attached. They are separate clients. Cookies set by a login performed in the browser during a test do not appear in a separately created request context, and a token obtained by an API call is not automatically presented by the page. Whenever a test mixes the two — seed a record over the API, then assert the console renders it — the identity behind each call has to be arranged on purpose. ## Carrying a role into an API context - **`page.request` / `context.request`.** These are bound to a browser context and use its cookies, so a call made through them is already the role that context is signed in as. Nothing extra is needed and nothing can drift. - **A standalone context seeded from the role file.** `request.newContext({ storageState: 'playwright/.auth/admin.json' })` starts with the cookies the capture saved. Useful when the calls should not disturb the page, or when there is no page at all. - **A context with explicit headers.** If the console authenticates with a bearer token rather than a cookie, `extraHTTPHeaders` on the new context carries it; the role then lives in the header, not the saved state. The first option is the smallest thing that works inside a UI test; the second is what a fixture uses when it wants the role available before or without a page. ## Reading state back out `apiRequestContext.storageState()` returns the context's current cookies and origins, and `storageState({ path })` writes them to a file. That is the reverse direction, and it is what makes an HTTP sign-in usable by the browser: 1. Create a context: `const ctx = await request.newContext()`. 2. Post the credentials to the console's session endpoint. 3. Write the result: `await ctx.storageState({ path: 'playwright/.auth/admin.json' })`. 4. Dispose the context, and let the role's specs load that file as their `storageState`. The state is the same document either way, which is why the two clients can be kept in step at all: a role is a file, and both a browser context and a request context can be built from it. ## Choosing between them | Situation | Reach for | Why | |---|---|---| | An API call inside a signed-in UI test | `page.request` | already the page's identity, nothing to wire | | A fixture preparing data before any page | a context from the role file | works without a browser context | | A sign-in performed over HTTP | `storageState({ path })` | produces the file the UI specs load | | Two roles acting in one test | one context per role | separate jars keep the identities apart | ## Pitfalls - Signing in through the page and expecting a separately created request context to follow. It will not; the two jars are unrelated. - Reusing one API context for two roles. Once it has the admin's cookies it keeps them; create a second context instead. - Forgetting `dispose()` on a context a fixture created. It holds a connection until it is disposed. - Letting the API call and the page disagree about identity — seeding data as the admin and then asserting the viewer's console shows it is a real scenario, but it must be deliberate rather than an accident of which client was handy. - Treating a written state file as a live session: it is a snapshot taken when it was written, and a request context built from it carries exactly those cookies and nothing newer.
- Why prefer page.request over a separately created context inside a UI test?It cannot fall out of step. `page.request` uses the browser context's cookies, so it is by construction the identity the test is signed in as, and a session refreshed during the test is reflected immediately. A separate context is a second jar that has to be seeded and kept correct.
- What exactly does apiRequestContext.storageState() return?The context's current cookies plus per-origin storage entries, the same document shape a browser context produces. Passing `{ path }` writes it to a file instead of only returning it, which is how an HTTP sign-in becomes a file that the role's browser specs can load.
saying these in an interview costs you the question
- Expecting a browser login to reach a separate request context
- Reusing one API context for two different roles
- Never disposing a request context created in a fixture
- Assuming a saved state file tracks a live session
- Seeding data as one role and asserting as another by accident