printhive/lib/flaresolverr.js
dlawler489 341731a946 Give downloads a deadline instead of hanging for ever
Neither the FlareSolverr call nor the CDN fetch set a timeout, so a stalled
connection hung the request indefinitely: the button sat on "Downloading…"
with nothing to reset it and no error to show. FlareSolverr calls now give up
15s after the deadline FlareSolverr itself was given (and a timeout counts as
retryable, like other transport failures), and a CDN download gives up after
three minutes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:50:48 +10:00

58 lines
2.5 KiB
JavaScript

// Minimal FlareSolverr client. FlareSolverr runs a headless browser that solves
// Cloudflare's anti-bot challenge and returns the resolved page HTML.
// https://github.com/FlareSolverr/FlareSolverr
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// opts: { maxTimeout, cookies: [{ name, value, domain }], retries }
async function solve(flaresolverrUrl, targetUrl, opts = {}) {
const endpoint = flaresolverrUrl.replace(/\/+$/, '') + '/v1';
const body = { cmd: 'request.get', url: targetUrl, maxTimeout: opts.maxTimeout || 60000 };
if (opts.cookies) body.cookies = opts.cookies;
// FlareSolverr serializes browser sessions and returns 500 on back-to-back
// requests, so retry transient failures with a growing backoff.
const retries = opts.retries ?? 4;
let lastErr;
for (let attempt = 0; attempt <= retries; attempt++) {
if (attempt > 0) await sleep(2000 * attempt);
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
// FlareSolverr is told to give up at maxTimeout; if it doesn't answer
// even then, stop waiting rather than hanging the request for ever.
signal: AbortSignal.timeout((opts.maxTimeout || 60000) + 15000),
});
if (res.status >= 500) { lastErr = new Error(`FlareSolverr HTTP ${res.status}`); continue; }
if (!res.ok) throw new Error(`FlareSolverr HTTP ${res.status}`);
const data = await res.json();
if (data.status !== 'ok' || !data.solution) throw new Error(`FlareSolverr: ${data.message || 'no solution'}`);
return data.solution.response;
} catch (err) {
lastErr = err;
if (!/HTTP 5\d\d|fetch failed|ECONNRESET|network|timeout|aborted/i.test(err.message)) throw err;
}
}
throw lastErr;
}
// Like solve(), but for endpoints that return JSON. A real browser wraps JSON
// in a <pre> tag, so strip tags before parsing.
async function solveJson(flaresolverrUrl, targetUrl, opts = {}) {
const html = await solve(flaresolverrUrl, targetUrl, opts);
// The browser wraps JSON in <pre> and HTML-encodes entities (notably & -> &amp;
// inside signed URLs), so strip tags and decode before parsing.
const text = html.replace(/<[^>]*>/g, '')
.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, '<')
.replace(/&gt;/g, '>').replace(/&amp;/g, '&')
.trim();
try {
return JSON.parse(text);
} catch {
throw new Error('FlareSolverr did not return parseable JSON.');
}
}
module.exports = { solve, solveJson };