const TRANSIENT_CODES = new Set([
"rate_limited",
"concurrency_limited",
"internal_error",
"upstream_unavailable",
"scrape_timeout",
]);
async function readWithRetry(body, maxAttempts = 3) {
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch("https://api.reader.dev/v1/read", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.READER_KEY,
},
body: JSON.stringify(body),
});
const envelope = await res.json();
if (envelope.success) return envelope.data;
const code = envelope.error.code;
lastError = envelope.error;
// Permanent? Give up.
if (!TRANSIENT_CODES.has(code)) throw new Error(envelope.error.message);
// Rate-limited? Honor the server's hint.
if (code === "rate_limited") {
const retryAfter =
parseInt(res.headers.get("Retry-After") || "") ||
envelope.error.details?.retryAfterSeconds ||
5;
await sleep(retryAfter * 1000);
continue;
}
// Otherwise exponential backoff: 1s, 2s, 4s
await sleep(Math.pow(2, attempt) * 1000);
}
throw new Error(`Exhausted retries: ${lastError?.message}`);
}