fix(pcat-auth): two-armed post-goto poll — fail-fast on dead sites
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

The post-goto token poll ran a blind 20s wait regardless of whether
page.goto succeeded or threw. On a healthy goto the widget API call
fires within ~1-2s; on a failed goto the request either already went
through (rare) or never will (common). The 20s cap was the dominant
cost on failed-site attempts — verified tonight as a 28s "No token
after ..." log on auto-komplekt after page.goto ERR_TIMED_OUT.

* CAPTURE_POLL_AFTER_OK   = 10 (5s)  — token usually arrives in <2s
* CAPTURE_POLL_AFTER_FAIL = 4  (2s)  — brief grace then bail

Per-attempt worst case on a dead site: 10s goto + 2s grace = 12s
(was 10s + 20s = 30s). On a healthy site, well-known capture times
(3-5s) stay comfortably inside the 5s post-goto cap.
This commit is contained in:
2026-06-01 22:17:41 +03:00
parent dcf7e068c9
commit 286307155e

View File

@@ -32,7 +32,14 @@ const REDIS_SAFETY_BUFFER_S = 60; // don't serve from Redis if <60s of life left
const TOKEN_TTL = 600; // seconds — TWS- has no built-in expiry, refresh aggressively
const REFRESH_BUFFER = 90; // Refresh 90s before expiry
const CAPTURE_POLL_INTERVAL = 500; // ms
const CAPTURE_POLL_MAX = 40; // 40 × 500ms = 20s max wait
// Two-armed post-goto poll: a healthy goto means the widget JS already loaded
// and the API call typically fires within ~1-2s — short cap is enough. A
// failed goto means we lost the page, but the widget request may still have
// gone through before the navigation timeout — give a brief grace window
// and bail. Old single-armed 20s cap was the dominant cost on failed-site
// attempts (e.g. 10s page timeout + 20s blind poll = 30s wasted).
const CAPTURE_POLL_AFTER_OK = 10; // 10 × 500ms = 5s after a healthy goto
const CAPTURE_POLL_AFTER_FAIL = 4; // 4 × 500ms = 2s grace after goto failure
const PAGE_TIMEOUT = 10_000; // 10s — healthy partner sites load in <5s through
// the proxy; a longer wait only prolongs dead-port connects in capture retries.
const CONTEXT_CLOSE_TIMEOUT = 5_000;
@@ -634,18 +641,23 @@ export class PartsCatalogsAuthService implements OnModuleInit, OnModuleDestroy {
});
// Navigate — networkidle waits for widget JS to load + make API calls
let gotoOk = true;
try {
await page.goto(siteUrl, {
timeout: PAGE_TIMEOUT,
waitUntil: "networkidle",
});
} catch (navErr) {
// Navigation may timeout but JWT could still be captured
// Navigation may timeout but JWT could still be captured if the
// widget request fired before goto gave up — see two-armed poll
// constants above.
gotoOk = false;
this.logger.debug(`Navigation ended: ${(navErr as Error).message?.slice(0, 80)}`);
}
// Poll for token
for (let i = 0; i < CAPTURE_POLL_MAX; i++) {
// Poll for token — short cap after healthy goto, brief grace after fail
const pollMax = gotoOk ? CAPTURE_POLL_AFTER_OK : CAPTURE_POLL_AFTER_FAIL;
for (let i = 0; i < pollMax; i++) {
if (capturedToken) break;
await new Promise((r) => setTimeout(r, CAPTURE_POLL_INTERVAL));
}