PRACTICAL GUIDE / Playwright HAR WebSocket request capture

Find the WebSocket handshake that used to vanish from HAR

Capture WebSocket upgrade requests in Playwright HAR files, correlate them with frames, and distinguish a failed handshake from a silent application.

By The Testing AcademyUpdated August 7, 202621 min read
All field guides
In this guide6 sections
  1. Know exactly what the HAR can prove
  2. Record a socket request in a self-contained test
  3. Correlate a real handshake with frame and UI evidence
  4. Diagnose a missing request from the first broken boundary
  5. Separate an edge rejection from a service rejection
  6. Sanitize and roll out the new evidence safely
  7. Know when HAR capture is the wrong tool

What you will learn

  • Know exactly what the HAR can prove
  • Record a socket request in a self-contained test
  • Correlate a real handshake with frame and UI evidence
  • Diagnose a missing request from the first broken boundary

A live-price test fails because the page never receives its first update. The trace shows the click that opened the dashboard, and the console says the client tried to connect, but an older HAR contains no socket request at all. Without the handshake, the failure looks like an application timeout instead of a network rejection.

Playwright 1.61 closes that evidence gap by including WebSocket requests in HAR and trace recordings. The new entry is useful, but it is not a transcript of the conversation. A QA engineer still needs frame events, product assertions, and a clear recording boundary to explain what happened after the upgrade.

Know exactly what the HAR can prove

A WebSocket begins as an HTTP upgrade request. The client asks the server to switch protocols, usually with headers such as Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key, Sec-WebSocket-Version, and sometimes Sec-WebSocket-Protocol. A successful server commonly answers with status 101 and the corresponding upgrade headers. After that handshake, the connection carries WebSocket frames rather than ordinary HTTP request-response pairs.

That boundary explains the evidence model:

  • The HAR entry can identify the request URL, method, selected request and response headers, status, and available timing information.
  • The trace network view can place the request beside the actions that caused it.
  • page.on('websocket') tells the test that a page created a WebSocket.
  • webSocket.on('framesent') and webSocket.on('framereceived') expose frame payloads observed by Playwright.
  • webSocket.on('socketerror') and webSocket.on('close') identify transport failure and closure after creation.

Playwright's release note says HAR and trace recordings include WebSocket requests. It does not promise that a HAR becomes a complete, replayable message log. Keep that claim narrow. If the report shows a successful upgrade but the expected price.updated message never arrives, the request capture has done its job. The investigation moves to subscription frames, authentication expiry, server publication, or client message handling.

Version is part of the mechanism. tracing.startHar() and tracing.stopHar() became public in Playwright 1.60. WebSocket requests appeared in these recordings in 1.61. A suite pinned to 1.60 can produce a valid HAR through the tracing API while still omitting the WebSocket request. That near-miss is easy to misdiagnose because both features arrived in adjacent releases.

The recorder also has a time boundary. It sees activity after recording starts. If the application opens its socket during an early bootstrap script and the test calls startHar() after page.goto(), the handshake is already gone. The same ordering applies to frame listeners. Register listeners before the action that can create the connection.

Filtering is another boundary. A urlFilter is a glob or regular expression applied to stored requests. A filter for https://app.example.test/api/** does not necessarily include wss://stream.example.test/live. Match the actual WebSocket URL, including its scheme, host, path, and possible query string.

Finally, the HAR does not prove that the UI consumed a frame correctly. A server can send the right payload while a stale reducer, schema mismatch, or detached component drops it. The user-visible assertion remains the test oracle. Network evidence explains that assertion; it does not replace it.

Record a socket request in a self-contained test

The fastest way to prove your Playwright version and recording setup is a test with a routed WebSocket. It needs no external socket server, which removes DNS, TLS, and deployment variables from the first check. page.routeWebSocket() intercepts the connection and acts as the server because the handler does not call connectToServer().

This example starts HAR recording before the page creates a socket, observes both directions of the conversation, and then verifies that the request URL appears in the resulting file. It requires Playwright 1.61 or newer.

TypeScript
import { readFile } from 'node:fs/promises';
import { expect, test } from '@playwright/test';

type HarFile = {
  log: {
    entries: Array<{
      request: { method: string; url: string };
      response: { status: number };
    }>;
  };
};

test('records a WebSocket request and observes its messages', async ({ context, page }, testInfo) => {
  const harPath = testInfo.outputPath('socket-request.har');
  const sent: string[] = [];
  const received: string[] = [];

  page.on('websocket', socket => {
    socket.on('framesent', event => sent.push(event.payload.toString()));
    socket.on('framereceived', event => received.push(event.payload.toString()));
  });

  await page.routeWebSocket('ws://example.test/live', socket => {
    socket.onMessage(message => {
      if (message === 'subscribe:prices') {
        socket.send(JSON.stringify({ type: 'price.updated', value: 42 }));
      }
    });
  });

  await context.tracing.startHar(harPath, {
    mode: 'full',
    content: 'omit',
    urlFilter: /\/live(?:\?|$)/,
  });

  await page.setContent(`
    <output id="price">waiting</output>
    <script>
      const socket = new WebSocket('ws://example.test/live');
      socket.addEventListener('open', () => socket.send('subscribe:prices'));
      socket.addEventListener('message', event => {
        const message = JSON.parse(event.data);
        document.querySelector('#price').textContent = String(message.value);
      });
    </script>
  `);

  await expect(page.locator('#price')).toHaveText('42');
  await expect.poll(() => sent).toContain('subscribe:prices');
  await expect.poll(() => received.join('\n')).toContain('price.updated');

  await context.tracing.stopHar();

  const har = JSON.parse(await readFile(harPath, 'utf8')) as HarFile;
  const request = har.log.entries.find(
    entry => entry.request.url === 'ws://example.test/live',
  );

  expect(request).toBeTruthy();
  expect(request?.request.method).toBe('GET');

  await testInfo.attach('socket-request-har', {
    path: harPath,
    contentType: 'application/json',
  });
});

The test does not assert status 101 because the socket is fulfilled by Playwright's routing layer rather than a real HTTP server. Its purpose is to verify capture ordering and the presence of the request. A separate integration test should assert the real deployment's handshake behavior.

Run the proof without the rest of the suite:

Shell
npx playwright test tests/socket-har.spec.ts --project=chromium --workers=1

If TypeScript rejects startHar, the installed client predates 1.60. If the file is created but the ws://example.test/live entry is absent, print npx playwright --version; a 1.60 client explains the gap. If the UI remains on waiting, inspect the frame arrays before touching the HAR parser. The mock conversation itself failed.

The call to stopHar() is not optional. The file is finalized when stopHar() runs or when the disposable returned by startHar() is disposed. Explicit finalization makes test ownership obvious and ensures the parser does not read a partial file. Only one HAR recording can be active per browser context, so do not run two helpers that both call startHar() on the same shared context.

Correlate a real handshake with frame and UI evidence

A production-like test needs one identity that appears in the page action, WebSocket URL or subscription message, frame log, and assertion. Without it, a busy dashboard can open several sockets and the report may pair the wrong request with the wrong message.

The following pattern assumes the application accepts a case identifier in the dashboard URL and includes it in the WebSocket URL. Replace the example paths and message schema with the real contract. The Playwright APIs are complete and runnable.

TypeScript
import { readFile } from 'node:fs/promises';
import { expect, test } from '@playwright/test';

test('ties the live order update to its upgrade request', async ({ context, page }, testInfo) => {
  const caseId = `pw-${testInfo.testId}-${testInfo.retry}`;
  const harPath = testInfo.outputPath('live-order.har');
  const sockets: Array<{
    url: string;
    sent: string[];
    received: string[];
    errors: string[];
    closed: boolean;
  }> = [];

  page.on('websocket', socket => {
    const record = {
      url: socket.url(),
      sent: [] as string[],
      received: [] as string[],
      errors: [] as string[],
      closed: false,
    };
    sockets.push(record);

    socket.on('framesent', event => record.sent.push(event.payload.toString()));
    socket.on('framereceived', event => record.received.push(event.payload.toString()));
    socket.on('socketerror', error => record.errors.push(error));
    socket.on('close', () => { record.closed = true; });
  });

  await context.tracing.startHar(harPath, {
    mode: 'full',
    content: 'omit',
    urlFilter: /\/live\/orders(?:\?|$)/,
  });

  try {
    await page.goto(`/orders/live?case=${encodeURIComponent(caseId)}`);
    await expect(page.getByTestId('connection-state')).toHaveText('Connected');

    await page.getByRole('button', { name: 'Create test order' }).click();
    await expect(page.getByTestId('latest-order')).toContainText(caseId);

    await expect.poll(
      () => sockets.flatMap(socket => socket.received).join('\n'),
      { message: 'expected a correlated order update frame' },
    ).toContain(caseId);
  } finally {
    await context.tracing.stopHar();
  }

  const har = JSON.parse(await readFile(harPath, 'utf8'));
  const upgrade = har.log.entries.find((entry: {
    request: { url: string };
    response: { status: number };
  }) => entry.request.url.includes('/live/orders'));

  expect(upgrade, 'HAR should contain the order WebSocket request').toBeTruthy();
  expect(upgrade.response.status).toBe(101);

  await testInfo.attach('websocket-frames', {
    body: Buffer.from(JSON.stringify(sockets, null, 2)),
    contentType: 'application/json',
  });
  await testInfo.attach('websocket-request', {
    path: harPath,
    contentType: 'application/json',
  });
});

Three failures that look like “live order did not update” now have different evidence:

Example
Case A
HAR status: 401
WebSocket event: none
Received frames: 0

Case B
HAR status: 101
Sent frame: {"type":"subscribe","case":"pw-..."}
Received frames: 0

Case C
HAR status: 101
Received frame: {"type":"order.updated","case":"pw-..."}
UI: latest-order remained empty

Case A is a handshake or authentication problem. Check cookies, authorization, origin validation, and the server response. Case B reaches the socket but receives no publication; inspect subscription shape, broker routing, server logs, and the case identifier. Case C proves the transport delivered the relevant message, so the likely defect is in the browser application's parsing or state update.

Binary frames need a different recorder from JSON messages. The event payload can be a string or a Buffer; blindly calling toString() on compressed or proprietary bytes can produce misleading text and leak content. Keep a bounded structural record instead:

TypeScript
import { createHash } from 'node:crypto';

function summarizeFrame(payload: string | Buffer) {
  if (typeof payload === 'string') {
    return {
      kind: 'text',
      characters: payload.length,
      preview: payload.slice(0, 120),
    };
  }

  return {
    kind: 'binary',
    bytes: payload.byteLength,
    sha256: createHash('sha256').update(payload).digest('hex'),
  };
}

page.on('websocket', socket => {
  socket.on('framesent', event => {
    console.log('sent', summarizeFrame(event.payload));
  });
  socket.on('framereceived', event => {
    console.log('received', summarizeFrame(event.payload));
  });
});

The hash lets two attempts show whether they received identical bytes without publishing the bytes themselves. It does not validate the message schema. Decode a binary protocol only with the application's supported codec and retain a small allowlisted set of fields. Truncating text previews also needs an explicit marker so reviewers do not mistake a preview for the complete message.

This is more useful than adding a ten-second wait. A longer wait may help a genuinely slow publication, but it cannot turn status 401 into 101 or make a reducer consume a payload it rejects.

Be cautious with status 101 across intermediaries and HTTP versions. The real browser and proxy chain determine the observed handshake representation. Assert the documented contract of your deployment, not a copied status assertion from a different gateway. The essential evidence is that the request is present, the socket is created or rejected as expected, and the product outcome agrees with the frame chronology.

Diagnose a missing request from the first broken boundary

Start with version output:

Shell
npx playwright --version

The decisive thresholds are 1.60 for tracing.startHar() and 1.61 for WebSocket requests in HAR and trace. Keep @playwright/test, playwright, and any browser image tags aligned. A newer browser container does not add a recording feature to an older Node client.

Next, prove the recorder started before the socket. Place startHar() before page.goto() when the application connects during startup. If the connection starts after a user action, register recording and listeners before that click. Trace the action sequence rather than relying on log timestamps from different machines.

Then inspect the URL filter. Temporarily remove urlFilter in a private diagnostic run. If the socket appears, restore a regex that matches ws: and wss: URLs plus query parameters. This filter:

TypeScript
/\/live\/orders(?:\?|$)/

matches both /live/orders and /live/orders?token=.... A glob copied from an HTTP API path can miss a separate streaming host.

Confirm the application used a WebSocket. A page can fall back to server-sent events or polling while keeping the same “Connected” label. The WebSocket event listener should record at least one URL. For ordinary network requests, request.resourceType() can report values such as eventsource, fetch, and websocket. Capture the resource type during diagnosis instead of inferring transport from a UI badge.

Separate request absence from immediate closure. If page.on('websocket') fires and close follows without frames, the socket existed. Record socketerror, the HAR response, and server logs. A protocol mismatch, origin rejection, expired token, or proxy idle policy can close the connection before the first application message.

Subprotocol negotiation deserves its own check. A server can accept the upgrade with status 101 while selecting no application protocol, or it can select a protocol the new client does not understand. The browser may then close quickly, leaving the same empty-UI symptom as an authorization failure. Read the request and response headers case-insensitively:

TypeScript
type HarHeader = { name: string; value: string };

function headerValue(headers: HarHeader[], name: string): string | undefined {
  return headers.find(
    header => header.name.toLowerCase() === name.toLowerCase(),
  )?.value;
}

const requests = har.log.entries.filter(
  (entry: { request: { url: string } }) =>
    entry.request.url.includes('/live/orders'),
);

expect(requests, 'expected one socket attempt').toHaveLength(1);

const [entry] = requests;
expect(
  headerValue(entry.request.headers, 'Sec-WebSocket-Protocol'),
).toContain('orders.v2');
expect(
  headerValue(entry.response.headers, 'Sec-WebSocket-Protocol'),
).toBe('orders.v2');

Do not assert a subprotocol if the application does not define one. When it does, this header pair proves what the page offered and what the server selected. It does not prove that later messages followed the selected schema, so retain at least one sanitized subscription and response frame.

Multiple matching HAR entries are also meaningful. A client often reconnects after an early close. Using find() and accepting the first status 101 can hide a loop in which five sockets open and die before the UI gives up. Count attempts, preserve their order, and record closure events:

Example
09:14:02.114  wss://stream.example.test/live/orders  101  closed, no frames
09:14:03.208  wss://stream.example.test/live/orders  101  closed, no frames
09:14:05.391  wss://stream.example.test/live/orders  401  no WebSocket event
UI state: Reconnecting

That sequence points toward expiring credentials or a reconnect implementation that loses authentication. A single final screenshot would show only “Reconnecting,” and a parser that kept one HAR entry would miss the transition from successful upgrades to rejection. Give each attempt an index in the frame attachment and compare its socket URL with the matching HAR entry.

Retries at the Playwright Test level add another axis. testInfo.retry distinguishes a rerun of the whole test from an in-page socket reconnect. Put the retry number in the artifact path or metadata, and never merge frames from retry zero with the HAR from retry one. A passing retry can establish a fresh authenticated session, which makes the combined evidence falsely suggest that the first attempt received frames.

Check context ownership last. A recorder attached to one browser context cannot capture a socket opened in another context. Popups stay in their parent context, but a separate persistent browser profile or independently connected client may not. Log the page URL, context page count, and socket URL under the same test ID.

For traces recorded through Playwright Test config, open the retained artifact:

Shell
npx playwright show-trace test-results/live-order-chromium/trace.zip

Use the Network tab to find the socket URL and align its start with the triggering action. Use the action snapshot to confirm the expected page initiated the workflow. The trace is stronger than HAR alone because it preserves test chronology, but a frame attachment is still the clearest record of application messages.

Separate an edge rejection from a service rejection

Two failed upgrades can produce nearly identical test output. The HAR contains one GET for the expected wss: URL, the response status is 401 or 403, no WebSocket event is emitted, and the UI remains on “Connecting.” In one case the edge layer rejected the request before it reached the WebSocket service. In the other, the edge forwarded it and the service rejected the credentials, origin, account, or requested protocol. Both are handshake failures, but they belong to different teams and require different fixes.

Start with the HAR entry, but do not stop at its status. Read the final request URL after sanitization rules are understood, the offered upgrade and subprotocol headers, the response status, the selected response headers, and the attempt's timing position relative to the user action. A healthy entry for a deployment that uses an application subprotocol has status 101 and the protocol value selected by the server. A broken entry has the rejection status and no socket frames. Those values define the boundary that failed. They still do not identify which server produced the rejection.

The producer is separated by correlation across layers. Match the test's case identity and attempt window to the ingress record. Then determine whether that ingress record selected an upstream and whether the application logged receipt of the same attempt. If the edge records a local policy or authentication rejection and there is no upstream or application receipt, the edge path owns the failure. If the ingress forwarded the attempt and the service records a deliberate rejection, the service or identity contract owns it. If an upstream was selected but no application receipt exists, the connection between those layers needs investigation. Exact log field names differ by platform, so use the identifiers your system already approves rather than inventing a test-only header a browser WebSocket cannot send.

Response decoration is only supporting evidence. A gateway may add an identifier to every response, including responses produced by the application. A service response may pass through a proxy that rewrites the body and strips headers. A generic JSON error can come from either layer. The server header is especially weak because infrastructure often removes or normalizes it. Timing is also misleading: a fast rejection is not automatically an edge rejection, and a slow one is not automatically application work. Only a matched ingress disposition plus application receipt establishes the producer.

The same rule applies to an apparent 502 or 503. The status could represent an edge with no healthy upstream, an upstream that closed before completing the handshake, or a service that intentionally returned that status. The HAR proves what reached the browser. It does not reveal the internal hop that chose the response. Preserve the response status and safe correlation fields, then use deployment logs to locate the last layer that handled the attempt normally.

Attempt identity must survive reconnects. A page that makes three connection attempts can receive an edge rejection, then a service rejection, then a successful upgrade. Do not hand another team a single status selected with find(). Provide the ordered attempt index, sanitized URL, status, socket-event presence, close or error observation, and the associated case identity. Timestamps help align systems, but they are not enough when clocks differ or many workers reconnect together. The case identity and attempt order prevent a log record from a neighboring test being used as proof.

There is a second authentication boundary after a successful upgrade. Some applications accept the HTTP upgrade and authenticate or authorize the subscription in the first application messages. In that design the HAR shows 101 for both a healthy and a rejected user. The separator moves to frames: a healthy run sends the expected subscription and receives the documented acknowledgement or business event, while a broken run receives an application error frame or closes before acknowledgement. Routing a 101 incident to the edge team solely because the UI says “Unauthorized” wastes the strongest evidence in the report.

Roll this triage path into an existing suite in dependency order. First land the sanitized attempt record and frame summary without changing when HAR is captured. Next ensure failure artifacts are finalized and retained under unique test paths. Then add the client capability check and enable WebSocket request capture for a small canary group. Only after the artifact is reliable should the edge and service teams add their corresponding correlation lookup to the runbook. If all parts land together, an empty record could mean an old client, a late recorder, a filter miss, a discarded file, or a genuine absent request.

The canary should include one known successful upgrade and one controlled rejection already supported by the test environment. The success proves that recording starts early enough and that the filter includes the socket host. The rejection proves that a non-101 entry survives sanitization and reaches the report. Do not create a new production bypass or special authentication mode for this purpose. Use an existing test account or contract whose expected rejection is safe and understood.

Ownership starts with the test team because it controls capture ordering. Its handoff should contain Playwright client version, test and retry identity, recording start boundary, sanitized filter description, ordered HAR entries, offered and selected subprotocol where applicable, and the bounded frame chronology. The edge owner adds the matched ingress disposition and upstream choice. The WebSocket service owner adds request receipt and its stable rejection reason. The frontend owner takes the case when the handshake and relevant frames are healthy but the product state does not change.

This evidence has a concrete maintenance cost. Correlation requires searchable logs with compatible retention, and sanitized artifacts must preserve enough fields to join records without exposing credentials. An aggressive allowlist can remove the only safe identifier, while a permissive one can leak cookies or signed URLs. Review the join fields with security and platform owners before rollout, then test the sanitizer with both success and rejection entries.

Handshake producer triage does not catch a message that is valid on the wire but wrong for the current UI state. A service can publish an event for an old account, stale document, or superseded sequence after a perfect 101 response. Only correlation inside the application payload and the user-visible assertion can catch that semantic error.

Sanitize and roll out the new evidence safely

A WebSocket request can carry more sensitive data than its empty body suggests. Authentication may appear in cookies, an Authorization header, a signed query parameter, or Sec-WebSocket-Protocol. The Origin header can reveal internal domains. Response headers can expose infrastructure names and sticky-session cookies.

content: 'omit' removes persisted resource content. It does not promise to remove URLs, cookies, or headers. Apply a retention policy before uploading the artifact.

An allowlist is safer than trying to remember every secret header:

TypeScript
import { readFile, writeFile } from 'node:fs/promises';

type Header = { name: string; value: string };

const allowedHeaders = new Set([
  'connection',
  'upgrade',
  'origin',
  'sec-websocket-version',
]);

function keepAllowed(headers: Header[]): Header[] {
  return headers.filter(header => allowedHeaders.has(header.name.toLowerCase()));
}

export async function sanitizeHar(input: string, output: string) {
  const har = JSON.parse(await readFile(input, 'utf8'));

  for (const entry of har.log.entries) {
    const url = new URL(entry.request.url);
    for (const key of url.searchParams.keys()) {
      url.searchParams.set(key, '[redacted]');
    }

    entry.request.url = url.toString();
    entry.request.headers = keepAllowed(entry.request.headers ?? []);
    entry.response.headers = keepAllowed(entry.response.headers ?? []);
    entry.request.cookies = [];
    entry.response.cookies = [];
    delete entry.request.postData;
  }

  await writeFile(output, JSON.stringify(har, null, 2), 'utf8');
}

This utility deliberately removes every query value, even values that seem harmless. Keep the raw HAR in a restricted, short-lived location when engineers need it for the incident. Upload only the sanitized derivative to a broader CI report. Do not overwrite the sole raw copy during an active investigation because sanitization can remove the field that explains an authentication failure.

Roll out capture to a small set of socket tests first. HAR files add storage and processing time. Full-mode recording across an asset-heavy suite can produce large artifacts even when the target is one socket, so use a narrow filter and failure-based retention. Measure artifact size and test duration before enabling it on every run.

Give each parallel test a unique HAR path through testInfo.outputPath(). A shared name such as network.har lets workers overwrite one another or attach the wrong file. Keep one recording per test context, and always finalize it in finally so a failed assertion still produces evidence.

Pin Playwright 1.61 or newer in every job that relies on the socket entry. During a mixed-version migration, attach the client version to the result and treat “entry unavailable on old client” as a capability difference, not a product failure. Remove that branch once all runners converge.

The trade-off is storage and exposure. Better handshake evidence shortens triage, but it also captures more network metadata. A small, filtered, access-controlled HAR plus a redacted frame summary is usually more useful than an unrestricted archive of the whole browser session.

Know when HAR capture is the wrong tool

Do not use a HAR entry as proof that the server sent the expected business event. The upgrade can succeed while subscriptions, broker permissions, or message schemas fail. Observe frames and assert the UI.

Do not expect routeFromHAR() to replay a stateful socket conversation. Use routeWebSocket() for deterministic message mocking. If TLS negotiation, proxy routing, load balancing, or the real streaming service is the subject, do not mock that layer at all.

Avoid recording after the failing action. A late recorder cannot recover the earlier handshake. Configure capture before navigation or before the user trigger.

Skip full-session HAR retention when a WebSocket event and a few sanitized frames already answer the question. More evidence is not automatically better. Large artifacts slow uploads, broaden secret exposure, and make reviewers search through noise.

Do not put raw frame payloads into a report without a data review. Chat messages, account updates, market data, and support notifications may contain personal or commercially sensitive values. Redact at the structured-message level and retain only fields needed to identify the event and case.

Finally, do not upgrade a stable suite solely to make one old HAR look fuller without testing the bundled browser changes. Playwright versions move browser revisions as well as client features. Validate the normal browser matrix, introduce the recording capability in a focused change, and keep the product assertions unchanged. That separation makes any new failure explainable.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 7, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why is my WebSocket missing from a Playwright HAR file?

Check the client version and recording window first. WebSocket requests were added to HAR and trace recordings in Playwright 1.61, and a recorder started after the socket opened cannot capture that earlier request.

Does the HAR contain every WebSocket message?

No. A recorded WebSocket request gives you handshake-level network evidence, while sent and received messages should be observed through Playwright's WebSocket frame events. Keep the two records under the same test identity.

Can routeFromHAR replay a WebSocket conversation?

HTTP HAR replay is not a substitute for a stateful frame exchange. Use `routeWebSocket()` to mock or proxy WebSocket messages, and use a real service when transport behavior itself is under test.

Which HAR mode should I use to diagnose a failed WebSocket upgrade?

Choose `mode: 'full'` for diagnosis because minimal mode intentionally omits timing, cookies, security, and other fields not needed for HAR routing. Pair it with `content: 'omit'` when response bodies are unnecessary.

Is a WebSocket HAR safe to upload from CI?

Assume it contains secrets until a review proves otherwise. Upgrade URLs, cookies, authorization headers, origins, and negotiated protocol values can expose credentials or internal topology even when body content is omitted.