In Playwright, how do page.request and the request fixture differ in the cookies they send?
answer
- Two flavours of the same client
- One shares the browser's jar
- Sharing runs in both directions
- The fixture starts empty each test
- Isolation is sometimes the point
basics
~20 spage.request and context.request share the browser context's cookie jar in both directions: they send its cookies and store any Set-Cookie they receive. The request fixture is a separate APIRequestContext with its own cookies and no link to the browser.
solid answer
~40 sThere are two kinds of `APIRequestContext`. The one reached through `page.request` or `context.request` is **bound to a browser context** — `page.request` is the same object as `context.request` for that page's context. It fills the `Cookie` header from the context's jar, so the server sees the same session the browser is in, and it writes any `Set-Cookie` from the response back into that jar, so a login performed over HTTP leaves the next `page.goto()` signed in. The `request` fixture, and anything from `playwright.request.newContext()`, is **isolated**: it starts empty, carries only what you configure with `extraHTTPHeaders` or `storageState`, and cookies it receives never reach a page. Pick by one question: must this call be the browser's session, or must it deliberately not be?
code
typescript · 14 linesimport { test, expect } from '@playwright/test';
test('a city saved over HTTP appears on the dashboard', async ({ page }) => {
await page.goto('/dashboard');
// Same cookie jar as the page, so the server sees the signed-in user.
const saved = await page.request.post('/api/favourites', {
data: { city: 'Bergen' },
});
await expect(saved).toBeOK();
await page.reload();
await expect(page.getByRole('listitem', { name: 'Bergen' })).toBeVisible();
});go deeper
Remember that page.request runs as the page's user while the request fixture is a separate client, and pick page.request when the call must be signed in.
Explain that sharing works both ways: the bound client sends the context's cookies and writes Set-Cookie back, and that the fixture starts with an empty jar every test.
Diagnose the silent failure where a seeded record never appears because the seed went out anonymously, and know when isolation is deliberately the right choice.
Set the convention for which surface each kind of call uses, so authorization tests cannot pass by accidentally borrowing a signed-in session.
Playwright exposes the same HTTP client class in two flavours, and the difference between them is the cookie jar. Getting this wrong is the classic reason a seeded record never appears on screen. ## Two kinds of APIRequestContext - **Bound to a browser context**: `context.request`, and `page.request`, which returns the very same instance as `context.request` for the context that page belongs to. - **Isolated**: the `request` fixture, and any context you build yourself with `playwright.request.newContext()`. The API surface is identical — the same `get`, `post`, `fetch` and options on both. Only the session differs. ## What sharing the cookie jar means, in both directions For the bound flavour, sharing runs two ways: 1. **Outbound**: the `Cookie` header is populated from the browser context before the request leaves, so a call made after the browser signed in is authenticated automatically, with no header to copy. 2. **Inbound**: a `Set-Cookie` on the response updates the browser context's cookies. Sign in with `context.request.post('/api/session', ...)` and the very next `page.goto('/dashboard')` loads as that user. That second direction is what makes the bound context a legitimate way to arrange a session, not merely a way to read state. ## The isolated fixture The `request` fixture starts with an empty jar every test. It knows nothing about `page` even when the same test also uses one; the two are separate clients that happen to point at the same server. Its identity comes only from what you gave it: - `extraHTTPHeaders` from the config or `test.use`, typically an `Authorization` header. - `httpCredentials` for HTTP basic auth. - `storageState` when you build the context yourself — cookies captured from an authenticated context, which is how an isolated client can act as a signed-in user. Isolation is a feature. A test that must prove an endpoint rejects an anonymous caller needs a client that is definitely not carrying the browser's session, and the fixture is exactly that. ## Choosing between them | Need | Use | |---|---| | Seed or check state as the user the browser is signed in as | `page.request` / `context.request` | | Have the HTTP response's cookies apply to the page | `context.request` | | Call as a different user, a service account, or anonymously | the `request` fixture or your own context | | A pure API test with no browser at all | the `request` fixture | | Deliberately test an unauthenticated or wrong-role call | the `request` fixture | ## Carrying a session into an isolated context Storage state is interchangeable between the two worlds. `apiRequestContext.storageState()` returns (or writes) the cookies an API context has collected, and `browserContext.storageState()` does the same for a browser context; either value can be handed to `playwright.request.newContext({ storageState })`. That is the supported way to give an isolated client a signed-in identity. Note that only the cookies matter to it — an HTTP client has no localStorage, so token-in-localStorage schemes need the token put on `extraHTTPHeaders` instead. ## The trap, on a weather dashboard A test signs in through the UI, seeds a favourite city with the `request` fixture, reloads the dashboard, and sees nothing. Every call returned 2xx, so nothing looks wrong. What happened is that the seed went out on the isolated jar: the server either rejected it as anonymous, or stored it against a different principal. Switching that one call to `page.request.post(...)` makes it the browser's session and the city appears. The reverse trap also exists: a test that intends to check an endpoint's authorization uses `page.request`, unwittingly sends the signed-in cookies, gets a 200, and reports that the anonymous case is fine. ## Things worth remembering - `page.request` and `context.request` are the same object for a given context — nothing about the page narrows it. - The bound client both reads and writes browser cookies. - The fixture is per test and disposed with the test; the bound one lives as long as its browser context. - Neither one goes through `page.route` handlers you registered for the page; these are requests made by the test process, not by the page.
- How do you give an isolated Playwright APIRequestContext the same session the browser has?Capture the state and pass it in. `await context.storageState({ path })` on the browser context, or `await apiContext.storageState()` on an API context, produces cookies both worlds accept; hand the value or path to `playwright.request.newContext({ storageState })`. For a bearer-token scheme, set the token on `extraHTTPHeaders` instead, since an HTTP client has no localStorage.
- Are calls made through page.request intercepted by page.route handlers?No. Route handlers intercept requests the page itself issues; `page.request` calls come from the test process and go straight to the server. That is often what you want — a seeding call should not be caught by the mock a test installed for the UI — but it surprises people who assume one interception layer covers everything.
- When is the isolated request fixture the correct choice inside a browser test?When the call must not be the browser's session: probing that an endpoint rejects an anonymous caller, acting as a second user or a service account, or seeding through an admin token the UI user does not hold. Isolation is the point, not a limitation to work around.
saying these in an interview costs you the question
- Thinks the request fixture inherits the browser's cookies
- Says page.request and context.request are different clients
- Believes API responses cannot set cookies on the browser
- Uses page.request to test an anonymous call
- Expects page.route handlers to catch page.request calls