# Network Interception and Mocking

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Authentication, Network and APIs
Source: https://www.devtutorials.ai/learn/playwright/auth-network-api/network-interception-and-mocking

> Watching requests and responses, waiting for a call with waitForRequest and waitForResponse, intercepting calls with page.route, URL patterns, mocking with route.fulfill, blocking with route.abort, changing requests with route.continue, chaining with route.fallback, editing real responses with route.fetch, error, empty, slow and offline states, context-wide routes, and HAR files.

## Network Interception and Mocking

- Modern pages load their data in the background. The page asks an **API** for products, orders or prices.
- A test can watch those calls, wait for them, and even **answer them itself**.
- That lets you test the empty list, the server error and the slow network, without breaking a real server.

**Simple lesson:** Playwright sits between the page and the server. It can watch, change or answer every call.

### A Real-World Example

- The Practice Shop's **Catalog** page asks `/api/products` for the list.
- What does a shopper see when the shop has no products? The real server always has some.
- This test answers the call with an empty list and checks the message.

- The page never reached the server for products. Playwright answered with `[]`.

### What You Will Learn

1. How a page talks to the server, and how to watch it.
2. Waiting for a call with `waitForResponse()` and `waitForRequest()`.
3. Intercepting calls with `page.route()` and URL patterns.
4. Mocking, blocking, changing and chaining requests.
5. Testing error, empty, slow and offline states.
6. When to mock, and when to use the real backend.

## How Pages Talk to Servers

### Requests and Responses

- A **request** is the page asking for something: a method (`GET`, `POST`), a URL, headers and maybe a body.
- A **response** is the answer. It has a **status code** (200 OK, 404 Not Found, 500 Server Error), headers and a body, often JSON.
- The browser's `fetch()` sends API requests. Playwright sees every one.

<figure className="plw-fig">
<svg viewBox="0 0 640 210" role="img" aria-labelledby="fig-plw-route" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-route">Animation: a request leaves the page, passes the route handler and reaches the server, and the response comes back. Then a second request is answered by route.fulfill without reaching the server</title>
  <rect x="15" y="60" width="140" height="90" rx="12" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} />
  <text x="85" y="100" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Page</text>
  <text x="85" y="120" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--mono)", fontSize: 11 }}>fetch()</text>
  <rect x="250" y="60" width="140" height="90" rx="12" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} />
  <text x="320" y="100" textAnchor="middle" style={{ fill: "var(--plw-purple)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>page.route()</text>
  <text x="320" y="120" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--mono)", fontSize: 11 }}>your handler</text>
  <rect x="485" y="60" width="140" height="90" rx="12" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }} />
  <text x="555" y="100" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Server</text>
  <text x="555" y="120" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--mono)", fontSize: 11 }}>/api/products</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <g><animateTransform attributeName="transform" type="translate" values="0 0;175 0;175 0;410 0;410 0" keyTimes="0;0.12;0.16;0.28;1" dur="10s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;1;1;0;0" keyTimes="0;0.12;0.16;0.28;0.3;1" dur="10s" repeatCount="indefinite" /><rect x="140" y="38" width="70" height="20" rx="5" style={{ fill: "var(--plw-blue)" }} /><text x="175" y="52" textAnchor="middle" style={{ fill: "#fff" }}>GET</text></g>
    <g opacity="0"><animateTransform attributeName="transform" type="translate" values="0 0;0 0;-410 0;-410 0" keyTimes="0;0.3;0.45;1" dur="10s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.29;0.3;0.45;0.47;1" dur="10s" repeatCount="indefinite" /><rect x="520" y="155" width="70" height="20" rx="5" style={{ fill: "var(--plw-green)" }} /><text x="555" y="169" textAnchor="middle" style={{ fill: "#fff" }}>200 [8]</text></g>
    <g opacity="0"><animateTransform attributeName="transform" type="translate" values="0 0;0 0;175 0;175 0" keyTimes="0;0.55;0.67;1" dur="10s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.54;0.55;0.7;0.72;1" dur="10s" repeatCount="indefinite" /><rect x="140" y="38" width="70" height="20" rx="5" style={{ fill: "var(--plw-blue)" }} /><text x="175" y="52" textAnchor="middle" style={{ fill: "#fff" }}>GET</text></g>
    <g opacity="0"><animateTransform attributeName="transform" type="translate" values="0 0;0 0;-175 0;-175 0" keyTimes="0;0.72;0.85;1" dur="10s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.71;0.72;0.97;1" dur="10s" repeatCount="indefinite" /><rect x="275" y="155" width="90" height="20" rx="5" style={{ fill: "var(--plw-orange)" }} /><text x="320" y="169" textAnchor="middle" style={{ fill: "#fff" }}>fulfill []</text></g>
  </g>
  <text x="320" y="200" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--sans)", fontSize: 12 }}>First call: passed to the server. Second call: answered by the handler.</text>
</svg>
<figcaption>A route handler stands between the page and the server. It can let a call pass, or answer it.</figcaption>
</figure>

### Watching Requests and Responses

- `page.on('request')` runs for every request. `page.on('response')` runs for every answer.
- `request.method()`, `request.url()` and `response.status()` tell you what happened.

- The page also calls a weather service on another site. The lab has no internet, so that call fails and the page shows **Weather unavailable**.

**Simple lesson:** `page.on('request')` and `page.on('response')` show every call the page makes.

## Waiting for Network Calls

### waitForResponse()

- Sometimes you need the **data** the page received, not just what it shows.
- `page.waitForResponse(url)` returns the response. Start waiting **before** the action that sends the call.

### waitForRequest()

- `page.waitForRequest()` returns the **request**. Use it to check what the page **sent**.
- A function works as a filter: `request => request.url().includes('/api/analytics')`.
- `request.postDataJSON()` reads a JSON body.

    request.url().includes('/api/analytics') && request.method() === 'POST');

  await page.goto('/catalog');
  const request = await requestPromise;

  expect(request.postDataJSON()).toEqual({ event: 'view', page: 'catalog' });
});
`}
/>

Start `waitForResponse()` or `waitForRequest()` **before** the click or `goto()` that sends the call. If you start after, the call may already be done and the wait times out.

**Simple lesson:** make the promise first, act second, `await` the promise third.

## Intercepting with page.route()

### URL Patterns

- `page.route(pattern, handler)` catches every request whose URL matches.
- A **glob** string: `**` matches anything, including slashes. `'**/api/products'` matches the URL `https://practice.devtutorials.ai/api/products`.
- A glob must match the **whole** URL. `'**/api/products'` does **not** match `/api/products?category=Shoes`. Add `*` at the end: `'**/api/products*'`.
- A **regular expression** (`/\/api\/products/`) or a **function** (`url => url.pathname === '/api/products'`) also works.
- Register routes **before** `goto()`. Calls made before the route exists are not caught.

### Mocking a Response with route.fulfill()

- `route.fulfill({ json })` answers with JSON and status 200.
- Add `status` for errors, `body` and `contentType` for other content, `headers` for custom headers.

- Mocking a **third-party** service, like the weather API, is very common. Your tests should not fail because someone else's server is down.

### Blocking Requests with route.abort()

- `route.abort()` makes the request fail, as if the network broke.
- Use it to block **analytics**, ads and trackers in tests, or to test what happens when one call fails.

    console.log('failed:', request.url(), request.failure()?.errorText));

  await page.goto('/catalog');
  await expect(page.getByRole('status').first()).toHaveText('8 products');
  await expect(page.getByText('No recommendations right now')).toBeVisible();
});
`}
/>

### Changing Requests with route.continue()

- `route.continue()` sends the request on to the server. You can change it on the way: `url`, `method`, `headers` or `postData`.
- Here, the page's orders call gets an `Authorization` header, so the server treats it as Ben's.

### Chaining Handlers with route.fallback()

- If several routes match, the one added **last** runs first.
- `route.fallback()` passes the request to the next matching handler. `route.continue()` skips the others and goes to the server.
- `{ times: 1 }` makes a route work only once. `page.unroute()` removes it.

**Simple lesson:** `fulfill` answers, `abort` fails, `continue` sends on, `fallback` asks the next handler.

## Modifying Real Responses

### route.fetch() Then fulfill()

- Sometimes you want **real** data with one small change.
- `route.fetch()` gets the real response. Change the JSON, then `route.fulfill({ response, json })`.
- The status and headers stay real. Only the body changes.

## Testing Hard-to-Reach States

### Server Errors and Slow Responses

- Real servers rarely fail when you want them to. A mock fails on demand.
- `status: 500` tests the error message. A delay before `route.continue()` tests the loading state.

### Going Offline

- `context.setOffline(true)` cuts the network for the whole context. Every call fails.
- `setOffline(false)` turns it back on. The config option `offline: true` starts offline.

In the lab, `page.goto()` still works while offline, because the practice pages are built in. A real offline browser cannot load new pages at all.

### Routes for the Whole Context

- `context.route()` works like `page.route()`, but for **every page** in the context, including popups and new tabs.
- Put it in a fixture or `beforeEach` to block trackers in all tests.

**Simple lesson:** mocks make rare states easy: errors, empty lists, slow and offline.

## Recording and Replaying (HAR)

### What a HAR File Is

- A **HAR** file is a recording of real network traffic, saved as JSON.
- `page.routeFromHAR('example.har', { update: true })` records the real answers once.
- Later runs, without `update`, answer every call from the file. No server needed.
- The lab cannot record real traffic, so here it is plain code.

```ts
test('catalog from a recording', async ({ page }) => {
  // First run: record. Then remove update: true to replay.
  await page.routeFromHAR('hars/catalog.har', {
    url: '**/api/**',
    update: true,
  });
  await page.goto('/catalog');
  await expect(page.getByRole('status').first()).toHaveText('8 products');
});
```

- `npx playwright open --save-har=hars/catalog.har https://practice.devtutorials.ai/catalog` records while you click around.
- `page.routeWebSocket()` does the same kind of mocking for **WebSockets**, the live connections that chats use.

## When to Mock

### Mocks Versus the Real Backend

- **Mock** when the state is hard to make: errors, empty data, slow calls, third-party services.
- **Use the real backend** for the main happy paths. A test full of mocks can pass while the real app is broken.
- A good mix: most tests hit a real test server. A few focused tests mock the rare cases.

If the real API changes its JSON shape, your mocks will not notice. Keep mock data close to real data, and use **API Testing** to check the real API shape.

**Simple lesson:** mock the rare and the external. Test the main flows for real.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Mock one third-party service your tests depend on, like payments or maps.
- Write one test each for a 500 error, an empty list and a slow response.
- Keep the main happy path on the real backend.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- `page.on('request' | 'response')` watches calls. `waitForResponse()` and `waitForRequest()` wait for one.
- `page.route(pattern, handler)` catches calls. Add it before `goto()`.
- Globs match the whole URL. Add `*` for query strings.
- `fulfill` answers, `abort` fails, `continue` sends on, `fallback` passes to the next handler.
- `route.fetch()` plus `fulfill({ response, json })` edits real data.
- `setOffline(true)` cuts the network. `context.route()` covers every page.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| `page.route()` after `page.goto()` | The first calls are not caught | Add routes before `goto()` |
| `'**/api/products'` for a URL with `?category=` | The route never matches | Use `'**/api/products*'` or a regex |
| Starting `waitForResponse()` after the click | The wait times out | Create the promise first, then click |
| A handler that never calls fulfill, abort, continue or fallback | The request hangs and the page waits forever | Always finish the route |
| Mocking every call in every test | Tests pass while the real app is broken | Mock rare and external cases only |

## Check Yourself

## Exercises

## What to Remember

- Watch with `page.on('request' | 'response')`. Wait with `waitForResponse()` and `waitForRequest()`.
- Routes go in before `goto()`. Globs match the whole URL.
- `fulfill`, `abort`, `continue`, `fallback`: answer, fail, send on, pass along.
- Mocks make error, empty, slow and offline states easy.
- Mock the rare and external. Keep main flows real.

Next: **API Testing**
