skip to content

What does expect(response).toBeOK() assert about a Playwright APIResponse?

level: juniorimportance: should knowfreq 44%

answer

  1. One check, on the status line
  2. The whole 2xx range passes
  3. No polling, nothing to wait for
  4. The failure message is the point
  5. Must be awaited like a matcher

basics

~20 s

It asserts that the response status is within the 200 to 299 range. Any 3xx, 4xx or 5xx fails, and the failure message carries the request log plus, for textual responses, the body Playwright received.

solid answer

~40 s

`await expect(response).toBeOK()` is the `APIResponse` matcher: it passes when the status sits in `200..299`, which is the same condition as `response.ok()`. A 4xx or 5xx fails it, and so does a 3xx you chose not to follow with `maxRedirects: 0`. It is **not** a web-first matcher — there is no polling and no timeout, because the response has already arrived and the matcher only inspects the value you hold. Its real advantage over `expect(response.status()).toBe(200)` is the failure message: Playwright prints the request log and, when the content type is textual, the beginning of the response body, so you see the server's error instead of a bare number. `expect(response).not.toBeOK()` covers the negative case, and the matcher is typed for `APIResponse`.

code

typescript · 14 lines
typescript
import { test, expect } from '@playwright/test';

test('an unknown city is rejected, a known one is served', async ({ request }) => {
  const missing = await request.get('/api/forecast', {
    params: { city: 'Atlantis' },
  });
  await expect(missing).not.toBeOK();

  const forecast = await request.get('/api/forecast', {
    params: { city: 'Bergen' },
  });
  await expect(forecast).toBeOK();
  expect((await forecast.json()).city).toBe('Bergen');
});

go deeper

for a junior

Remember that it passes for statuses 200 to 299, that it must be awaited, and that it is the first check to write after any API call.

for a middle

Explain that it wraps response.ok(), that it never polls because the exchange is already finished, and why its failure message beats comparing a status number.

for a senior

Show that you know where it is too loose — a create that must return 201 — and that you gate on it before parsing, so a broken endpoint reports as a bad status not a parse error.

for a principal

Frame the standard: which failures a suite must surface with the server's own message, so on-call engineers can read a report without rerunning anything locally.

`toBeOK` is the assertion Playwright provides for the objects its HTTP client returns. You write `await expect(response).toBeOK()` where `response` is an `APIResponse` — the value handed back by `request.get()`, `page.request.post()` or any other `APIRequestContext` call. ## What the matcher checks One thing only: that the response status is inside the range `200..299`. That is the identical condition exposed as `response.ok()`, so `toBeOK` is a readable, better-reporting wrapper around it rather than a different rule. Concretely: - `200`, `201` and `204` pass. - `301` or `302` fails — although with the default redirect following you rarely see one, since Playwright follows up to 20 redirects and returns the final response. Set `maxRedirects: 0` and the 3xx arrives intact, and then it fails. - `400`, `401`, `404` and `500` all fail. ## Why it beats a bare status comparison The assertion is nothing special; the failure message is. When it fails, Playwright attaches the request log and, if the response content type is textual, the first part of the response body to the error. A broken endpoint therefore reports the server's own message rather than a number that says only that the number was wrong. | Assertion | On failure you see | |---|---| | `await expect(response).toBeOK()` | The request log and the response body, with the failing status | | `expect(response.status()).toBe(200)` | `Expected: 200, Received: 500` and nothing else | | `expect(response.ok()).toBeTruthy()` | `Expected: truthy, Received: false` — the least informative of the three | There is one behavioural difference worth knowing: `toBeOK` accepts any 2xx, so an endpoint that switches from `200` to `201` keeps passing. When the exact code is the point of the test, assert the status directly instead. ## It does not retry Playwright's locator matchers such as `toBeVisible` poll until a timeout because the page keeps changing. `toBeOK` does not, and cannot usefully: the HTTP exchange is finished and the response is a fixed value in memory. Re-checking it would only produce the same answer. If a request needs retrying, that belongs on the call — `maxRetries` on the request, or a polling wrapper around the whole call — not on this matcher. It is still asynchronous, because reading the body for the failure message is asynchronous. Forgetting the `await` gives you a floating promise and an assertion that never fails, which is one of the quieter ways an API test goes green while the endpoint is broken. ## Negation and typing `await expect(response).not.toBeOK()` asserts the opposite, and is the compact way to record that a request was rejected. The matcher is typed: it checks it was handed an `APIResponse` and raises a type error otherwise, so the network `Response` object obtained from page events is not a valid target for it. ## Where it fits in a call A typical weather-dashboard case reads: 1. Send the call and hold the response: `const forecast = await request.get('/api/forecast', { params: { city: 'Bergen' } })`. 2. Gate on it: `await expect(forecast).toBeOK()`. 3. Only then parse: `const body = await forecast.json()`. The order matters. Parsing before the gate means a 500 that returned an HTML error page fails inside `json()` with a parse error, and the test reports a JSON problem instead of a dead endpoint. ## Common misuses - Treating it as a full assertion of correctness — it says the status was fine, nothing about the payload. - Expecting it to wait for a slow service to come up. - Dropping the `await`, so nothing is ever asserted. - Using it on a value that is not an `APIResponse`.

  • When would you assert the status directly in Playwright instead of using toBeOK()?
    When the exact code carries meaning. `toBeOK()` accepts the whole 2xx range, so it cannot tell `200` from `201` or `204`. A test that cares that a create returned `201`, or that a delete returned `204`, should assert `expect(response.status()).toBe(201)` and keep `toBeOK()` for the many calls where any success will do.
  • Why is expect(response).toBeOK() awaited when it never polls?
    The matcher is asynchronous because building its failure message reads the response body and the request log, and both are async. Skipping the `await` leaves a floating promise: the assertion never resolves inside the test, so a failing status cannot fail the test. Lint rules for floating promises catch this.

saying these in an interview costs you the question

  • Thinks toBeOK polls until the endpoint recovers
  • Believes it also validates the response payload
  • Says only 200 passes, not the whole 2xx range
  • Uses it without await, so nothing is asserted
  • Parses the JSON body before checking the status