feat(emex): Floxy residential proxy fallback for the flaky DataImpulse pool
DataImpulse (74.81.81.81, random port 10001-10099) intermittently throws connect-timeouts / resets, dropping real emex hits. Add a Floxy residential fallback (residential.floxy.io:12321): fetchEmexHtml now runs a provider schedule — 3 DataImpulse attempts (rotating port), then 2 Floxy attempts — and only falls back on transport errors (a definitive HTTP answer like 404 still stops the schedule). On by default; endpoint/creds overridable via EMEX_FLOXY_* env. Direct proxy-less last resort still gated by EMEX_DIRECT_FALLBACK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,14 @@ export class EmexService {
|
||||
portStart: number;
|
||||
portEnd: number;
|
||||
} | null;
|
||||
// Floxy residential fallback — used when the DataImpulse pool throws transport
|
||||
// errors. Single rotating endpoint (one port; IP rotates per request).
|
||||
private readonly emexFloxy: {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
} | null;
|
||||
private readonly emexDirectFallback: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -163,6 +171,22 @@ export class EmexService {
|
||||
this.proxyAgent = null;
|
||||
}
|
||||
|
||||
// Floxy residential fallback for when the DataImpulse pool flakes (connect
|
||||
// timeouts / resets). On by default; creds + endpoint overridable via env.
|
||||
const floxyEnabled = this.configService.get<string>("EMEX_FLOXY_FALLBACK", "true") === "true";
|
||||
if (floxyEnabled) {
|
||||
const fport = Number(this.configService.get("EMEX_FLOXY_PORT", 12321));
|
||||
this.emexFloxy = {
|
||||
host: this.configService.get<string>("EMEX_FLOXY_HOST", "residential.floxy.io"),
|
||||
port: Number.isInteger(fport) && fport >= 1 && fport <= 65535 ? fport : 12321,
|
||||
user: this.configService.get<string>("EMEX_FLOXY_USER", "d739255e819b"),
|
||||
pass: this.configService.get<string>("EMEX_FLOXY_PASS", "9092873ba4e0"),
|
||||
};
|
||||
this.logger.log(`EMEX Floxy fallback enabled: ${this.emexFloxy.host}:${this.emexFloxy.port}`);
|
||||
} else {
|
||||
this.emexFloxy = null;
|
||||
}
|
||||
|
||||
this.logger.log(`EMEX Service initialized with scraper path: ${this.scraperPath}`);
|
||||
}
|
||||
|
||||
@@ -269,7 +293,16 @@ export class EmexService {
|
||||
* without requiring authentication cookies.
|
||||
*/
|
||||
/** Build a fresh proxy agent on a random port from the pool (null if proxy off). */
|
||||
private newProxyAgent(): ProxyAgent | null {
|
||||
private newProxyAgent(provider: "dataimpulse" | "floxy" = "dataimpulse"): ProxyAgent | null {
|
||||
if (provider === "floxy") {
|
||||
if (!this.emexFloxy) return null;
|
||||
const { host, port, user, pass } = this.emexFloxy;
|
||||
return new ProxyAgent({
|
||||
uri: `http://${user}:${pass}@${host}:${port}`,
|
||||
connect: { timeout: 30000 },
|
||||
requestTls: { timeout: 30000 },
|
||||
});
|
||||
}
|
||||
if (!this.emexProxy) return null;
|
||||
const { host, user, pass, portStart, portEnd } = this.emexProxy;
|
||||
const port = Math.floor(Math.random() * (portEnd - portStart + 1)) + portStart;
|
||||
@@ -281,14 +314,19 @@ export class EmexService {
|
||||
}
|
||||
|
||||
private async fetchEmexHtml(url: string): Promise<string> {
|
||||
const maxAttempts = this.emexProxy ? 3 : 1;
|
||||
// Attempt schedule: DataImpulse (rotating port, fresh agent each try to dodge
|
||||
// a flaky sticky port — ~42% blip rate, undecoded-vin-rca.md EMEX #1) first,
|
||||
// then the Floxy residential fallback when the DataImpulse pool throws
|
||||
// transport errors. A definitive HTTP answer (e.g. 404) stops the schedule —
|
||||
// it's a real result, and a different proxy IP must not "retry" it away.
|
||||
const schedule: Array<"dataimpulse" | "floxy"> = [];
|
||||
if (this.emexProxy) schedule.push("dataimpulse", "dataimpulse", "dataimpulse");
|
||||
if (this.emexFloxy) schedule.push("floxy", "floxy");
|
||||
const maxAttempts = schedule.length || 1; // 0 → single proxy-less attempt
|
||||
let lastErr: unknown;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
// Fresh agent every attempt: a single sticky DataImpulse port flakes
|
||||
// intermittently ("fetch failed"/reset), silently dropping real emex hits
|
||||
// (~42% blip rate observed — undecoded-vin-rca.md, EMEX #1). A new agent
|
||||
// rotates the port and forces a new socket even on a single-port pool.
|
||||
const agent = this.newProxyAgent();
|
||||
const provider = schedule[attempt - 1]; // undefined when no proxy → direct
|
||||
const agent = provider ? this.newProxyAgent(provider) : null;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": EMEX_UA, Accept: "text/html,application/xhtml+xml" },
|
||||
@@ -298,6 +336,9 @@ export class EmexService {
|
||||
if (!res.ok) {
|
||||
throw new Error(`EMEX HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
if (provider === "floxy") {
|
||||
this.logger.log(`EMEX fetch via Floxy fallback succeeded for ${url}`);
|
||||
}
|
||||
return await res.text();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
@@ -313,15 +354,18 @@ export class EmexService {
|
||||
`${e.message} ${String(e.cause ?? "")}`,
|
||||
));
|
||||
if (!transient || attempt === maxAttempts) break;
|
||||
const next = schedule[attempt];
|
||||
const via =
|
||||
next === "floxy" && provider !== "floxy" ? "via Floxy fallback" : "with fresh proxy";
|
||||
this.logger.warn(
|
||||
`EMEX fetch transient error (attempt ${attempt}/${maxAttempts}) for ${url}: ${e.message} — retrying with fresh proxy`,
|
||||
`EMEX fetch transient error (attempt ${attempt}/${maxAttempts}, ${provider}) for ${url}: ${e.message} — retrying ${via}`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 300 * attempt));
|
||||
}
|
||||
}
|
||||
// Last resort (opt-in via EMEX_DIRECT_FALLBACK): one direct, proxy-less attempt
|
||||
// for when the whole pool is down. Off by default — it exposes the origin IP.
|
||||
if (this.emexProxy && this.emexDirectFallback) {
|
||||
// for when both proxy pools are down. Off by default — it exposes the origin IP.
|
||||
if ((this.emexProxy || this.emexFloxy) && this.emexDirectFallback) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": EMEX_UA, Accept: "text/html,application/xhtml+xml" },
|
||||
|
||||
Reference in New Issue
Block a user