Addressed freezing indefinitely when requests are aborted.
This commit is contained in:
+54
-28
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user