skip to content

HAR Record and Replay

Recording real traffic into a HAR archive and replaying it as the backend for later runs, including how to refresh a recording and what happens to requests it does not contain.

on this pageshow

explore

questions

5

In Playwright, what does browserContext.routeFromHAR() do to the requests a page makes?

level: juniorimportance: must knowfreq 62%

answer

  1. Real traffic saved to a file
  2. Recorded with a context option
  3. Replayed by a route handler
  4. Lookup by method and URL
  5. Missing entries abort by default

basics

~20 s

It installs a route that answers matching requests from a previously recorded HAR archive instead of the real server, so the test replays saved traffic. Requests the archive does not contain are aborted by default.

solid answer

~40 s

`browserContext.routeFromHAR(har)` points a whole context at a HAR archive: each request a page makes is looked up in that file by method and URL, and the recorded status, headers and body are served back without touching the network. `page.routeFromHAR()` does the same for one page only. You get the archive either from the `recordHar` context option (`{ path: 'har/weather.har' }`), which writes the file when the context closes, or from the CLI with `npx playwright open --save-har=har/weather.har`. Replay does not consume entries, so a weather dashboard polling the forecast endpoint repeatedly keeps getting the same recorded response. Two options shape the edges: `url` narrows which requests the archive may answer, and `notFound` decides what a request with no entry does — `'abort'` by default, or `'fallback'` to pass it on.

code

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

test('forecast panel renders from a recorded session', async ({ page }) => {
  await page.routeFromHAR('har/weather.har', {
    url: '**/api/forecast**',
    notFound: 'abort',
  });

  await page.goto('/dashboard');
  await expect(page.getByTestId('forecast-high')).toHaveText('21');
});

go deeper

for a junior

Be able to say what a HAR file holds and that routeFromHAR replays it in place of the real server. Knowing that the recordHar context option produces the file is enough at this stage.

for a middle

Explain the mechanics: lookup by method and URL, entries that answer repeated requests, the file flushed on context close, and the difference between page and context scope.

for a senior

Show judgment about which endpoints an archive should carry and how a replaying suite behaves when the real service is unreachable, so CI never depends on a third party being up.

for a principal

Own the policy around archives as repository artefacts: where they live, who refreshes them, how large they may grow, and what must never be captured into one.

## What a HAR archive holds A **HAR** (HTTP Archive) file is a JSON log of HTTP traffic. Each entry records one exchange: the request method, URL, headers and post data, plus the response status, headers and body. Playwright can both write that log and read it back, which turns a single real session against the third-party forecast API behind a weather dashboard into a fixed backend that later runs replay offline. ## Producing the file There are two ways to record, and both flush the archive at the end of a session rather than as it goes: - **The `recordHar` context option** — `browser.newContext({ recordHar: { path: 'har/weather.har' } })`. Every request the context makes is logged, and the file appears when `context.close()` runs. Under Playwright Test you set it through `use: { contextOptions: { recordHar: { path: 'har/weather.har' } } }`, and the built-in `context` fixture closes at the end of each test. - **The CLI** — `npx playwright open --save-har=har/weather.har --save-har-glob='**/api/**' https://dashboard.example` drives a real browser by hand and saves the same format with no test code at all. `npx playwright codegen` accepts the same two flags. Besides `path`, `recordHar` takes `urlFilter` (a glob or `RegExp` limiting what is logged), `mode` (`'full'` or `'minimal'`) and `content` (`'omit'`, `'embed'` or `'attach'`), which decides whether response bodies are dropped, inlined into the JSON, or written as separate files beside the archive. ## Replaying the file `await context.routeFromHAR('har/weather.har')` installs a route across the whole browser context; `await page.routeFromHAR('har/weather.har')` scopes the same behaviour to one page. From then on Playwright looks each outgoing request up in the archive **by method and URL** and fulfils it from the recording — status, headers and body — without reaching the network. Two consequences follow: - Entries are **not consumed**. If the dashboard polls `/api/forecast?city=oslo` every thirty seconds, every poll is answered from that one recorded entry. - The lookup key is the URL as requested, so a recording made against one origin will not answer requests to another. `baseURL` differences and cache-busting query parameters both change the key. A relative `har` path is resolved against the **current working directory** of the run, not against the spec file, so build the path with `path.join(__dirname, ...)` if the suite might be started from another folder. ## The options at the edges | option | values | default | what it decides | |---|---|---|---| | `url` | glob or `RegExp` | every request | which requests the archive is allowed to answer | | `notFound` | `'abort'`, `'fallback'` | `'abort'` | what a request with no entry does | | `update` | boolean | `false` | re-record from the live network instead of serving the file | | `updateContent` | `'embed'`, `'attach'` | `'attach'` | where bodies go when re-recording | | `updateMode` | `'full'`, `'minimal'` | `'minimal'` | how much HAR detail is written back | `url` and `notFound` are the pair people confuse. `url` filters what the archive is even consulted about: a request outside the pattern is left alone and goes to the network or a later handler. `notFound` applies only to a request the archive **was** consulted about and could not answer. ## The usual workflow 1. Record once against the real forecast API, with `urlFilter` narrowed to the endpoints under test. 2. Commit the archive next to the spec — with its attachment files, or using a `.zip` path so the whole recording travels as a single file. 3. Replay it with `routeFromHAR` so CI never calls the third party, and re-record later by running the same call with `update: true`. ## Where it earns its keep - A dashboard that renders a dozen widgets from one large payload is tedious to stub by hand; a recording captures the response verbatim, including the fields nobody remembered. - The suite stops depending on a service you do not control, which removes a third party from your CI failure modes. - The scope is yours to pick: `page.routeFromHAR` lets one page replay while another page in the same context still talks to a local server. The cost is that an archive is a snapshot. It answers exactly what was recorded, and anything the app newly requests is a miss — which is why the `notFound` policy is a deliberate decision rather than a detail. `'abort'` makes an incomplete recording fail loudly; `'fallback'` quietly lets the request through to the real world.

  • How do you get a HAR file without writing any test code first?
    Drive the app by hand from the CLI: `npx playwright open --save-har=har/weather.har --save-har-glob='**/api/**' https://dashboard.example`. It launches a browser, records what you click through, keeps only the URLs matching the glob and writes the archive when you close it. `npx playwright codegen` takes the same two flags.
  • What is the difference between page.routeFromHAR() and browserContext.routeFromHAR()?
    Scope, and nothing else. The page form serves the archive to that one page, so a popup or a second page in the same context still reaches the network. The context form covers every page the context has or later opens. Options and matching behave identically in both.

A HAR archive is a tape of a conversation with the server: replay plays the same answers back on cue, and anything that was never said on tape has no answer at all.

saying these in an interview costs you the question

  • Thinks a HAR file replays automatically once it exists on disk
  • Believes each recorded entry can only be replayed once
  • Expects the archive to be written before the context closes
  • Assumes routeFromHAR proxies requests through to the real server
  • Thinks recording requires changing the application's network code
open as a page

In Playwright, what happens to a request that routeFromHAR() finds no matching entry for?

level: middleimportance: must knowfreq 52%

basics

~20 s

By default it is aborted, so the application sees a failed request rather than a response. Passing notFound: 'fallback' instead lets the request continue to the next matching route handler or to the real network.

open as a page

In Playwright, what changes when you pass update: true to routeFromHAR()?

level: middleimportance: should knowfreq 44%

basics

~20 s

The call stops serving from the file. Requests go to the real network, their traffic is recorded, and the HAR is rewritten when the context closes, so the same test that replays a recording can also refresh it.

open as a page

A Playwright HAR replay leaves the weather dashboard's forecast panel empty in CI though it passes locally — how do you diagnose it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Establish first whether the archive answered the request at all. Then compare the URL the app asks for against the recorded one, and check that the archive's response bodies actually travelled into CI rather than staying in uncommitted attachment files.

open as a page

How would you scope a Playwright recordHar capture before those archives are committed to a repository?

level: principalimportance: should knowfreq 28%

basics

~20 s

Decide before recording what belongs in the file: narrow urlFilter to the endpoints under test, pick minimal mode and a zip path so archives stay small and self-contained, and treat everything captured as permanently readable repository data.

open as a page