Skip to content

fix: replace playwright-core monkey patches with a public-API har engine - #64

Draft
vanilla-wave wants to merge 12 commits into
mainfrom
feat/har-zero-internals-engine
Draft

fix: replace playwright-core monkey patches with a public-API har engine#64
vanilla-wave wants to merge 12 commits into
mainfrom
feat/har-zero-internals-engine

Conversation

@vanilla-wave

@vanilla-wave vanilla-wave commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Problem

Playwright bundled all of lib/server/** into lib/coreBundle.js starting with playwright-core 1.60.0 (PRs microsoft/playwright#40057 + #40074; first bundled artifact 1.60.0-alpha-2026-04-07, first stable 1.60.0).

har/getPlaywrightCoreModule.ts resolves absolute file paths inside playwright-core and require()s them:

  • lib/server/dispatchers/localUtilsDispatcher — patched for harOpen / harLookup
  • lib/server/har/harRecorder — patched for onEntryFinished / flush

Both now throw MODULE_NOT_FOUND, so the whole HAR feature is dead on Playwright >= 1.60, and ./fixtures is affected transitively through mockNetworkFixturesBuilderharPatcher. @playwright/test@1.62 has no lib/ at all, so the second resolution branch is dead too. coreBundle's server namespace exports no HAR class, and 1.63.0-alpha is the same shape.

Worse, the failure is silent: patchInited = true is the first line of each init function, before getPlaywrightCoreModule() throws. In every worker the first four tests die with four different errors and tests 5+ register no transforms and throw nothing — the recorded https://base.url.placeholder is never rewritten back, every lookup returns noentry, and notFound: 'abort' aborts everything.

Solution

Stop reaching into playwright-core. har/getPlaywrightCoreModule.ts is deleted — no require() of internal paths remains.

initDumps() now calls installHarEngine(page), which wraps the public routeFromHAR on the client prototypes:

  • Replay — Playwright replays the dump itself, so request matching and response timing are unchanged. addHarOpenTransform is applied by handing Playwright an already-rewritten copy of the dump; addHarLookupTransform wraps the client-side LocalUtils.harLookup that Playwright's own HAR router calls.
  • RecordrouteFromHAR({update: true}) writes to a temporary path; the record-side transforms are applied to the written dump, which is only then moved to its final location. If anything fails the dump is simply missing instead of being committed with unscrubbed headers.
  • Fallback — for Playwright < 1.55 or a thin client, a userland replay on context.route() with a vendored port of Playwright's matcher (har/vendor/, Apache-2.0, LICENSE + NOTICE shipped). Announced once per process with a HAR engine degraded (...) warning.

Two record-side notes, both of which also applied before this change:

  • addHarRecorderTransform / addFlushTransform run after context.close() rather than during the test. Same Entry objects, same order, same result on disk. HarRecorder.onEntryFinished has had an empty body in every release from 1.22 to 1.63-alpha, so the old hook only ever worked through by-reference mutation; addFlushTransform was additionally dead on 1.60+ because export() calls _flush(), not the patched flush().
  • Response bodies live in separate blobs referenced by content._file, so they are not reachable from these hooks. Rewriting a body per request is what addHarLookupTransform's transformResult is for.

Compatibility

All four documented hooks keep their names, signatures, argument shapes, return contracts and first-call-wins latch semantics. initDumps, clearHeaders, replaceBaseUrlInEntry, setExtraHash, the path builders and all exported types are untouched. getPlaywrightCoreModule was never exported from har/index.ts nor listed in the exports map, so deleting it is invisible from outside.

Additive only: installHarEngine, resetHarTransforms, setFixtureHarTransforms.

The oldest supported version is 1.23 — earlier ones have no routeFromHAR at all, so the declared peer range ^1.22 is wider than what actually works.

Verification

Record → post-process → replay round trip with all four hooks firing, same bytes on 1.58.1 and 1.62.1.

Validated against a large real consumer (Yandex Tracker, ~2400 committed dumps) on Playwright 1.62.1:

  • a 483-test slice: 444 passed / 14 failed vs 438 / 20 on the trunk baseline — zero failures unique to this change
  • an independent adversarial check ran the missing control (this engine on Playwright 1.58.2) and found the engine to be a no-op at fixed Playwright version
  • the rewritten temporary dump is byte-equivalent to the old in-memory transform (JSON.stringify(oldHar) === JSON.stringify(newHar), all zip blobs identical)
  • the userland fallback never activates on 1.58.2 or 1.62.1

Tests

har/ had no tests at all, which is why both the 1.60 breakage and the long-dead addFlushTransform went unnoticed. Added 41 unit tests and 2 integration ones:

  • har/vendor/__tests__/harBackend.test.ts — the matching semantics the whole feature rests on: exact url + method, byte-exact POST bodies, multipart-boundary tolerance, the header tie-break that markIdenticalRequests / setExtraHash depend on, document order on a tie, redirect following, the POST → GET downgrade, redirect cycles, _file sidecars and the path-traversal guard, base64 bodies, archives.
  • har/vendor/__tests__/zip.test.ts — reads an archive recorded by Playwright itself (checked in as a fixture: it carries the data-descriptor flag, so the sizes are only valid in the central directory), round-trips, and rewrites it.
  • har/vendor/__tests__/harJsonStringify.test.ts — the serializer is pinned to Playwright's own layout so that re-recording a dump does not produce a formatting-only diff.
  • har/engine/__tests__/transformRegistry.test.ts — the first-call-wins latch, per-call independence, fixture vs legacy precedence, reset.
  • har/engine/__tests__/harPostProcessor.test.ts — record-side transforms applied to the written dump, ordering between the per-entry and flush passes, orphaned blobs dropped, uncompressed dumps.
  • tests/har/har.test.ts — a real record → transform → replay round trip against a local origin server, asserting that all four hooks fired, that the dump on disk is scrubbed and rewritten, and that replay serves the transformed body while the origin (now answering LIVE) is never reached. Plus a recorded navigation redirect.

CI

.github/workflows/har-matrix.yml runs the unit and integration tests against Playwright 1.49.1, 1.55.0, 1.58.1, 1.59.0, 1.60.0 and latest — deliberately spanning the 1.59 → 1.60 bundling boundary that broke the previous implementation — plus a nightly, non-blocking next canary. next is published daily, and the 1.60 bundling landed there 34 days before it reached a stable release.

@gravity-ui

gravity-ui Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🚀 Prerelease version published!

Install this PR version:

npm i --save-dev @gravity-ui/playwright-tools@2.0.2-beta.a98d26971c986205c17a06245b9260462d6f9dcc.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant