fix: proxy pool now actively distributes IPs across sessions

- fetchWithRetry: use proxy on every attempt (not just fallback after direct)
- fetchWithSessionRetry: call acquire() on attempt 0 so new sessions get
  a least-loaded proxy assigned, enabling IP spread across concurrent users
- Update docstrings to reflect new strategy

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-17 04:26:26 +07:00
co-authored by Claude
parent 9bd6acddc5
commit 519fe3af83
+28 -17
View File
@@ -105,9 +105,10 @@ export interface FetchWithRetryResult {
} }
/** /**
* Execute an upstream `fetch` with automatic retry and proxy fallback. * Execute an upstream `fetch` with automatic retry and proxy rotation.
* *
* Strategy: direct first → proxy-1 on failure → rotate proxy on failure. * Strategy: proxy pool on every attempt (rotate each retry).
* Falls back to direct when no pool is available.
* Logs every failure to `console.warn` so the operator can diagnose without * Logs every failure to `console.warn` so the operator can diagnose without
* the error body leaking to the downstream client. * the error body leaking to the downstream client.
*/ */
@@ -122,18 +123,21 @@ export async function fetchWithRetry(
let usedProxy = false; let usedProxy = false;
for (let attempt = 0; attempt < 3; attempt++) { for (let attempt = 0; attempt < 3; attempt++) {
if (attempt === 0) { if (proxyPool && proxyPool.size > 0) {
init.proxy = undefined; // direct // Proactive proxy usage on every attempt, rotating each time
} else if (attempt === 1 && proxyPool && proxyPool.size > 0) { if (attempt === 0) {
usedProxy = true; init.proxy = proxyPool.getProxyUrl()!;
init.proxy = proxyPool.getProxyUrl()!; usedProxy = true;
} else if (attempt === 2 && proxyPool && proxyPool.size > 0) { } else {
const next = proxyPool.rotate(); const next = proxyPool.rotate();
if (!next) break; if (!next) break;
usedProxy = true; init.proxy = proxyPool.getProxyUrl()!;
init.proxy = proxyPool.getProxyUrl()!; usedProxy = true;
}
} else { } else {
break; // No proxy pool — direct only
init.proxy = undefined;
usedProxy = false;
} }
try { try {
@@ -213,9 +217,11 @@ function classifyFetchErrorSafe(error: unknown): {
/** /**
* Execute an upstream `fetch` using a session-sticky proxy with retry. * Execute an upstream `fetch` using a session-sticky proxy with retry.
* *
* Strategy: session-sticky proxy (via SessionProxyPool) on attempt 1; on * Strategy: acquire() on attempt 0 (least-loaded assignment), then
* failure the session's proxy is marked failed — which auto-rotates if the * getProxyUrl() on subsequent attempts (reads the existing or rotated proxy).
* failure threshold is exceeded — and the request retries with the new proxy. * On persistent failure the session's proxy is marked failed — which
* auto-rotates if the failure threshold is exceeded — and the request
* retries with the new proxy.
* *
* SSE streams: the initial request is retried normally. Once the response body * SSE streams: the initial request is retried normally. Once the response body
* starts streaming, mid-stream errors are **not** retried; the session is * starts streaming, mid-stream errors are **not** retried; the session is
@@ -242,7 +248,12 @@ export async function fetchWithSessionRetry(
let lastError: unknown; let lastError: unknown;
for (let attempt = 0; attempt < maxRetries; attempt++) { for (let attempt = 0; attempt < maxRetries; attempt++) {
const proxyUrl = sessionPool.getProxyUrl(sessionId); // First attempt: acquire() assigns a proxy to this session (least-loaded
// distribution so many concurrent users spread across different IPs).
// Subsequent attempts: getProxyUrl() reads the existing (or rotated) proxy.
const proxyUrl = attempt === 0
? sessionPool.acquire(sessionId)
: sessionPool.getProxyUrl(sessionId);
if (proxyUrl) { if (proxyUrl) {
init.proxy = proxyUrl; init.proxy = proxyUrl;
} }