fix(pcat): retry transient proxy/network failures in fetchWithAuth
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

The parts-catalogs HTTP client only retried request timeouts and 401/403;
undici network errors ("TypeError: fetch failed" — a dropped/reset
DataImpulse proxy connection) were thrown on the first attempt. During the
cold-JWT window (outside 09:00-19:00 Istanbul) the proxy drops frequently,
so a single click would surface an empty/slow category that returns full
parts on a manual retry — and could even mark a populated parent group
unavailable when child discovery hit the blip.

Retry transient transport errors (timeout + undici network failures) with a
short backoff, re-acquiring a session each attempt. Definitive HTTP
responses (re-thrown as `Error: HTTP <code> …`, e.g. 400 "list of parts is
empty") are still thrown immediately — they are real answers, not blips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 01:29:38 +03:00
parent aa4d055c77
commit 93d3b08992

View File

@@ -231,9 +231,28 @@ export class PartsCatalogsService {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} from ${endpoint}: ${text.slice(0, 200)}`);
} catch (err) {
if ((err as Error).name === "TimeoutError") {
this.logger.warn(`Timeout on ${endpoint}, attempt ${attempt + 1}`);
if (attempt < maxRetries) continue;
const e = err as Error & { cause?: unknown };
// Retry transient TRANSPORT failures only: request timeouts and undici
// network errors ("TypeError: fetch failed" — a dropped/reset DataImpulse
// proxy connection). A definitive HTTP response (e.g. 400 "list of parts
// is empty") is re-thrown from the try block as `Error: HTTP <code> …`
// and must NOT be retried — it is a real answer, not a blip.
const transient =
e.name === "TimeoutError" ||
e.name === "TypeError" ||
/fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN|socket hang up|other side closed|terminated|UND_ERR/i.test(
`${e.message} ${String(e.cause ?? "")}`,
);
if (transient && attempt < maxRetries) {
this.logger.warn(
`Transient transport error on ${endpoint} (attempt ${attempt + 1}/${maxRetries + 1}): ${e.message}${
e.cause ? ` [cause: ${String(e.cause)}]` : ""
}`,
);
// Brief backoff so a momentarily-flaky proxy port can recover; the next
// loop iteration re-acquires a session (round-robin across the pool).
await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
continue;
}
throw err;
}