Addressed freezing indefinitely when requests are aborted.

This commit is contained in:
2026-09-02 17:27:26 +02:00
parent ba922bb385
commit 3e8ed79ccc
4 changed files with 139 additions and 48 deletions
+54 -28
View File
@@ -111,43 +111,69 @@ async function wait(seconds) {
async function requestJson(path, params = {}, attempt = 0) {
const url = buildUrl(path, params);
return limiter.schedule(async () => {
const res = await fetch(url.toString(), { headers: headers() });
const contentType = res.headers.get('content-type') || '';
// Only the actual throttled network call belongs inside limiter.schedule() - this has
// maxConcurrent: 1, and every retry path below used to recurse back into requestJson()
// (and so back into limiter.schedule()) from *inside* that same callback. Bottleneck won't
// start a second job while the first is still running, and the first can't finish until
// its awaited recursive call starts - a guaranteed deadlock the instant a retry was ever
// needed (e.g. a single 429), freezing every reddit API call behind it for good. Retrying
// now happens here, after the scheduled call has actually returned and freed its slot.
let res;
if (res.status === 429 || res.status === 503) {
if (attempt >= MAX_RETRIES) {
throw new Error(`Reddit rate-limited request after ${attempt} retries (${res.status}) for '${path}'`);
}
const retryAfter = Number(res.headers.get('retry-after')) || 2 ** attempt;
logger.warn(`Rate limited by reddit (${res.status}) for '${path}', retrying in ${retryAfter}s`);
await wait(retryAfter);
return requestJson(path, params, attempt + 1);
try {
// config.proxy.retryFlipped: once this request has already failed once, retry it via
// whichever of proxied/direct is NOT what it'd normally get - see proxy.js
res = await limiter.schedule(async () => fetch(url.toString(), {
headers: headers(),
proxyFlip: config.proxy.retryFlipped && attempt > 0,
}));
} catch (error) {
// fetch() has no timeout of its own beyond undici's fairly generous defaults (see
// proxy.js) - this catches both that and genuine network errors (DNS, connection
// refused/reset, etc.), neither of which was retried before
if (attempt >= MAX_RETRIES) {
throw new Error(`Failed to reach reddit after ${attempt} retries for '${path}': ${error.message}`);
}
if (!contentType.includes('json')) {
// reddit serves an HTML interstitial/block page instead of JSON when it decides
// the request looks like a bot - this is a distinct failure mode from a genuine
// 4xx and should be logged/handled separately rather than treated as "bad request"
const hint = cookieHeader() ? ' - session cookie may have expired, refresh config/cookies.js' : ' - likely blocked or interstitial page (consider setting config.reddit.cookies)';
logger.warn(`Failed to reach reddit for '${path}', retrying: ${error.message}`);
throw new Error(`Reddit returned non-JSON response (${res.status}, content-type '${contentType || 'none'}') for '${path}'${hint}`);
return requestJson(path, params, attempt + 1);
}
const contentType = res.headers.get('content-type') || '';
if (res.status === 429 || res.status === 503) {
if (attempt >= MAX_RETRIES) {
throw new Error(`Reddit rate-limited request after ${attempt} retries (${res.status}) for '${path}'`);
}
const data = await res.json();
const retryAfter = Number(res.headers.get('retry-after')) || 2 ** attempt;
if (!res.ok) {
const message = data?.message || data?.reason || JSON.stringify(data).slice(0, 200);
logger.warn(`Rate limited by reddit (${res.status}) for '${path}', retrying in ${retryAfter}s`);
throw new Error(`Reddit returned ${res.status} for '${path}': ${message}`);
}
await wait(retryAfter);
return data;
});
return requestJson(path, params, attempt + 1);
}
if (!contentType.includes('json')) {
// reddit serves an HTML interstitial/block page instead of JSON when it decides
// the request looks like a bot - this is a distinct failure mode from a genuine
// 4xx and should be logged/handled separately rather than treated as "bad request"
const hint = cookieHeader() ? ' - session cookie may have expired, refresh config/cookies.js' : ' - likely blocked or interstitial page (consider setting config.reddit.cookies)';
throw new Error(`Reddit returned non-JSON response (${res.status}, content-type '${contentType || 'none'}') for '${path}'${hint}`);
}
const data = await res.json();
if (!res.ok) {
const message = data?.message || data?.reason || JSON.stringify(data).slice(0, 200);
throw new Error(`Reddit returned ${res.status} for '${path}': ${message}`);
}
return data;
}
async function getUser(username) {