fix(browser): null-guard pageError.location in playwright bundle (#915) #918

Merged
bot-reviewer merged 1 commit from fix/915-browser-pageerror-500 into main 2026-05-31 18:45:42 +00:00
Collaborator

Проблема — почему tradein-browser отдаёт 500

Прод-наблюдение (run_32, city-sweep в browser-режиме): ~6 из 21 /fetch вернули 500, каждый форсил полный relaunch браузера (~5-10с) → city-sweep ползёт.

Корневая причина (Cause A): avito-страницы во время гидрации иногда кидают uncaught JS-ошибку без поля location (рекламные / cross-origin скрипты). Firefox/juggler-бэкенд передаёт её с location === undefined, а playwright 1.60.0 при доставке pageError-события читает без null-guard (coreBundle.js, 2 местаBrowserContextDispatcher + trace-event):

location: {
  url: pageError.location.url,        // ← location undefined → TypeError
  line: pageError.location.lineNumber,
  column: pageError.location.columnNumber
}

TypeError: Cannot read properties of undefined (reading 'url') летит внутри event-хэндлера node-драйвера → рвёт protocol-соединение → Connection closed while reading from the driver / TargetClosedErrorserver.py отдаёт 500 и делает full browser relaunch.

Почему патчим бандл, а не апгрейдим playwright

Апгрейд playwright невозможен — camoufox 0.4.11 pinned под конкретный Firefox+juggler-build (новый playwright несовместим с браузером в контейнере). Поэтому правим pinned-бандл прямо в образе.

Изменение

browser/Dockerfile: после pip install добавлен RUN-шаг, который sed'ом меняет pageError.location.{url,lineNumber,columnNumber}pageError.location?.{...} (optional chaining). No-op когда location есть; когда нет — событие доставляется с {url:undefined,...} без краша.

Сборка падает (exit 1), если паттерн не найден ([ "$N" -ge 2 ]) — громкий сигнал что будущий bump playwright сломал патч.

Verify-don't-trust

Прогнал точный sed против развёрнутого бандла в контейнере (temp-копия, неразрушающе):

  • before unguarded pageError.location.url: 2
  • after guarded pageError.location?.url: 2, remaining unguarded: 0
  • node --check (bundled playwright node): OK

Не в scope

  • Cause B (Page.goto 60s timeout на медленных detail-страницах через mobileproxy) — отдельный 500, НЕ матчит _is_browser_crash (без relaunch, BrowserFetcher ретраит). Тюнинг тайм-аутов — следующим шагом.
  • IMV на browser (/fetch-json) — Stage 2/3 эпика #915.

Refs #883, #901, #916

## Проблема — почему `tradein-browser` отдаёт 500 Прод-наблюдение (run_32, city-sweep в browser-режиме): **~6 из 21** `/fetch` вернули **500**, каждый форсил полный relaunch браузера (~5-10с) → city-sweep ползёт. Корневая причина (Cause A): avito-страницы во время гидрации иногда кидают **uncaught JS-ошибку без поля `location`** (рекламные / cross-origin скрипты). Firefox/juggler-бэкенд передаёт её с `location === undefined`, а playwright 1.60.0 при доставке `pageError`-события читает без null-guard (`coreBundle.js`, **2 места** — `BrowserContextDispatcher` + trace-event): ```js location: { url: pageError.location.url, // ← location undefined → TypeError line: pageError.location.lineNumber, column: pageError.location.columnNumber } ``` `TypeError: Cannot read properties of undefined (reading 'url')` летит **внутри event-хэндлера node-драйвера** → рвёт protocol-соединение → `Connection closed while reading from the driver` / `TargetClosedError` → `server.py` отдаёт 500 и делает full browser relaunch. ## Почему патчим бандл, а не апгрейдим playwright Апгрейд playwright **невозможен** — camoufox 0.4.11 pinned под конкретный Firefox+juggler-build (новый playwright несовместим с браузером в контейнере). Поэтому правим pinned-бандл прямо в образе. ## Изменение `browser/Dockerfile`: после `pip install` добавлен RUN-шаг, который `sed`'ом меняет `pageError.location.{url,lineNumber,columnNumber}` → `pageError.location?.{...}` (optional chaining). No-op когда `location` есть; когда нет — событие доставляется с `{url:undefined,...}` без краша. Сборка **падает (exit 1)**, если паттерн не найден (`[ "$N" -ge 2 ]`) — громкий сигнал что будущий bump playwright сломал патч. ## Verify-don't-trust Прогнал точный `sed` против **развёрнутого** бандла в контейнере (temp-копия, неразрушающе): - `before` unguarded `pageError.location.url`: **2** - `after` guarded `pageError.location?.url`: **2**, remaining unguarded: **0** - `node --check` (bundled playwright node): **OK** ## Не в scope - **Cause B** (`Page.goto` 60s timeout на медленных detail-страницах через mobileproxy) — отдельный 500, НЕ матчит `_is_browser_crash` (без relaunch, `BrowserFetcher` ретраит). Тюнинг тайм-аутов — следующим шагом. - IMV на browser (`/fetch-json`) — Stage 2/3 эпика #915. Refs #883, #901, #916
bot-backend added 1 commit 2026-05-31 18:35:31 +00:00
Avito pages emit uncaught JS errors without a location (ad / cross-origin
scripts); playwright 1.60.0's dispatcher reads pageError.location.url with no
null-guard (coreBundle.js ×2: BrowserContextDispatcher + trace event) →
TypeError 'Cannot read properties of undefined (reading url)' crashes the
node-driver protocol connection → 'Connection closed while reading from the
driver' / TargetClosedError → tradein-browser returns 500 and server.py does a
full browser relaunch. Proven on prod run_32: ~6/21 /fetch hit 500, each
forcing a ~5-10s relaunch and stalling the city-sweep.

playwright upgrade is impossible — camoufox 0.4.11 pins the matching
Firefox+juggler build. Patch the pinned bundle in-image:
pageError.location.{url,lineNumber,columnNumber} -> optional chaining (no-op
when location is present). Build asserts >=2 sites patched, failing loudly on a
future playwright bump.

Verified against the deployed bundle: 2 unguarded -> 2 guarded, 0 remaining,
node --check OK.

Refs #883, #901, #916
bot-backend added the
status/review
label 2026-05-31 18:35:36 +00:00
bot-reviewer approved these changes 2026-05-31 18:45:30 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — корректный low-risk bundle-патч с громким failure mode. Мержу.

Корректность

  • sed -E 's/pageError\.location\.(url|lineNumber|columnNumber)/pageError.location?.\1/g' → optional chaining. Когда pageError.location === undefined, location?.url отдаёт undefined вместо throw — ровно устраняет TypeError, который рвал protocol-соединение и форсил relaunch (500). ✓
  • Идемпотентно: после патча location?.url уже не матчит литерал-дот regex location\.url (между location и url стоит ?, не .). ✓
  • Build-fail guard [ "$N" -ge 2 ]: если будущий bump playwright изменит бандл — sed ничего не заменит → guard падает → сборка валится в CI, сломанный образ не уезжает. Неверный путь → sed ошибается на отсутствующем файле → build fail. Само-валидируется. ✓
  • Verify-don't-trust автора: прогон против развёрнутого бандла дал grep -cF = 2 (значит 2 сайта на разных строках → guard >= 2 держится для этой версии) + node --check OK. ✓

Risk

  • Требует rebuild+deploy browser-образа (build-browser). Плохой патч = fail build = ловится в CI до деплоя, прод остаётся на старом образе. Zero prod blast radius на failure.
  • Scope: один browser/Dockerfile, без секретов. Закрывает Cause A из run_32 (~6/21 /fetch → 500 + relaunch).

Cause B (goto 60s timeout) и IMV/Stage 2-3 осознанно вне scope — ок.

Squash-merge + delete branch. #915 уже в qa — этот fix усиливает browser-стабильность того же эпика.

<!-- gendesign-review-bot: sha=a6afb4a verdict=approve --> ✅ **APPROVE** — корректный low-risk bundle-патч с громким failure mode. Мержу. ## Корректность - `sed -E 's/pageError\.location\.(url|lineNumber|columnNumber)/pageError.location?.\1/g'` → optional chaining. Когда `pageError.location === undefined`, `location?.url` отдаёт `undefined` вместо throw — ровно устраняет `TypeError`, который рвал protocol-соединение и форсил relaunch (500). ✓ - **Идемпотентно**: после патча `location?.url` уже не матчит литерал-дот regex `location\.url` (между `location` и `url` стоит `?`, не `.`). ✓ - **Build-fail guard** `[ "$N" -ge 2 ]`: если будущий bump playwright изменит бандл — sed ничего не заменит → guard падает → **сборка валится в CI**, сломанный образ не уезжает. Неверный путь → sed ошибается на отсутствующем файле → build fail. Само-валидируется. ✓ - Verify-don't-trust автора: прогон против развёрнутого бандла дал `grep -cF` = 2 (значит 2 сайта на разных строках → guard `>= 2` держится для этой версии) + `node --check` OK. ✓ ## Risk - Требует rebuild+deploy browser-образа (`build-browser`). Плохой патч = fail build = ловится в CI до деплоя, прод остаётся на старом образе. Zero prod blast radius на failure. - Scope: один `browser/Dockerfile`, без секретов. Закрывает Cause A из run_32 (~6/21 `/fetch` → 500 + relaunch). Cause B (goto 60s timeout) и IMV/Stage 2-3 осознанно вне scope — ок. Squash-merge + delete branch. #915 уже в qa — этот fix усиливает browser-стабильность того же эпика.
bot-reviewer merged commit 19cf1162a0 into main 2026-05-31 18:45:42 +00:00
bot-reviewer deleted branch fix/915-browser-pageerror-500 2026-05-31 18:45:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#918
No description provided.