In Playwright, how do you assert on the payload your app sent to an API?
answer
- The Request is the outgoing surface
- Subscribe before the action, as always
- Parsed body, raw string or bytes
- Parse the URL for query parameters
- headers() hides cookie and security headers
basics
~10 sCapture the outgoing Request with page.waitForRequest or a page.on('request') listener, then read request.postDataJSON() for the parsed body, request.postData() for the raw string, and method(), url() and headers() for the rest.
solid answer
~40 sThe `Request` object is the assertion surface for what the browser sent. `page.waitForRequest(urlOrPredicate)` resolves with one — same subscribe-then-act ordering as `waitForResponse` — while `page.on('request')` collects every request into an array. On the object: `request.method()` is the verb, `request.url()` carries the query string (parse it with `new URL(...)`), `request.postDataJSON()` returns the body parsed as JSON or as key/value pairs for `application/x-www-form-urlencoded`, and `null` when there is no body. `request.postData()` gives the raw string and `request.postDataBuffer()` the bytes for binary uploads. `request.headers()` is lower-cased but omits cookie and other security-related headers, so use `await request.allHeaders()` when those matter. That is enough for a weather dashboard test to prove the save-location call carried the right coordinates and content type.
code
typescript · 9 linesconst savePlace = page.waitForRequest(
(request) => request.url().includes('/api/places') && request.method() === 'POST',
);
await page.getByRole('button', { name: 'Save location' }).click();
const request = await savePlace;
expect(request.postDataJSON()).toMatchObject({ lat: 51.5, lon: -0.13 });
expect(request.headers()['content-type']).toContain('application/json');go deeper
Know that the Request object describes what the browser sent, and that postDataJSON gives you the body already parsed while method and url cover the rest.
Explain when to use postDataJSON, postData and postDataBuffer, why headers() is incomplete, and why the query string should be read through the URL parser rather than matched as text.
Show the failure modes you have hit: an empty collector read too early, a preflight matched instead of the real call, a redirect hop asserted by mistake, traffic escaping into a popup.
Decide where payload contracts belong. Asserting every field of every request in the browser suite duplicates contract tests and slows change; pick the handful of payloads the UI alone can get wrong.
## Getting hold of the Request There are three routes to the object, and they suit different tests: 1. `page.waitForRequest(urlOrPredicate)` — a promise for one outgoing request. Same ordering rule as `waitForResponse`: create it, perform the action, then await it. 2. `page.on('request', handler)` — every request the page issues. Push them into an array and filter afterwards when the assertion is about a set of calls. 3. `response.request()` — when you already awaited the response and now want the call behind it, which keeps a single wait doing both jobs. ## What a Request exposes | Call | Returns | | --- | --- | | `request.method()` | the HTTP verb as a string | | `request.url()` | the full URL including the query string | | `request.postData()` | the raw body as a string, or `null` | | `request.postDataJSON()` | the body parsed as JSON, or form fields as an object | | `request.postDataBuffer()` | the raw body bytes | | `request.headers()` | lower-cased headers, minus cookie and security headers | | `request.allHeaders()` | promise of the complete header set | | `request.resourceType()` | `fetch`, `xhr`, `document`, `image` and friends | | `request.failure()` / `request.response()` | the transport error, or the matching `Response` | | `request.redirectedFrom()` / `redirectedTo()` | neighbouring hops of a redirect chain | ## Reading the body - `postDataJSON()` is the everyday call. It parses a JSON body, and for `application/x-www-form-urlencoded` it returns a key/value object of the fields. It returns `null` when the request carries no body at all, which is why it is useless on a `GET`. - `postData()` returns the raw string. Reach for it when the payload is not plain JSON — newline-delimited records, a GraphQL document, or a body you actually expect to be malformed. - `postDataBuffer()` returns bytes, which is the right call for a file upload or a binary encoding. - Assert with `toMatchObject` rather than a deep equality on the whole body, so the test does not break the day the app adds an optional field. ## Query strings and headers - Parse `request.url()` with `new URL(...)` and assert on `searchParams`. String-matching a whole URL couples the test to parameter order and encoding. - `request.headers()` is convenient and synchronous but incomplete: cookie and other security-related headers are excluded by design. `await request.allHeaders()` returns everything. - Assert on the two or three headers that carry meaning — `content-type`, the scheme of an `authorization` header, a tenant or locale header — not the whole map, which drifts between browser versions and engines. ## Traps that bite - **Reading the collector too early.** An array filled by a `page.on('request')` handler is empty if you assert on the next line. Await a `waitForRequest` promise, or assert after an exchange you already awaited. - **Redirects.** A redirected call produces more than one `Request`; follow `redirectedFrom()` and `redirectedTo()` so you know which hop your assertion is examining. - **Preflights.** A CORS `OPTIONS` request shares the URL and carries no body, so a URL-only matcher can hand you the wrong object. Require the method in the predicate. - **Popups.** Traffic from a window opened by the page belongs to that popup's own `Page`; a listener on the opener sees nothing. Listen at the browser-context level when either page can issue the call. - **Async handlers.** Playwright calls your listener but does not await it, so keep handlers to pushing values into an array rather than doing asynchronous work. ## Putting it together A save-location flow on the weather dashboard is a good shape to rehearse: subscribe to the `POST` to `/api/places`, click the save button, await the request, then assert the parsed body carries the coordinates the user picked and that the content type is JSON. That verifies the contract the frontend is responsible for — the payload it produced — independently of whatever the backend chose to answer.
- What does request.postDataJSON() give you for a form-encoded submission?An object of the decoded field names and values, because Playwright parses `application/x-www-form-urlencoded` bodies as well as JSON. If you need the exact wire format instead — ordering, encoding, duplicate keys — read `request.postData()` and assert on the raw string.
- Why not assert on the whole header map a request sent?Most of it is the browser's business — accept, encoding, client hints, user agent — and it changes between engines and versions. Assert the few headers your app is responsible for setting, and let the rest vary.
saying these in an interview costs you the question
- Expecting postDataJSON to return the query string of a GET
- Asserting on a collector array immediately after the click
- Deep-equalling a whole request body in every test
- Assuming request.headers() contains the cookie header
- Ignoring that a preflight OPTIONS matches the same URL