[object Object]

← back to Jf Fabrics Recrawl

jf_fabrics recrawl: survive HTTP 429 throttle (retry+backoff, concurrency 1, 1.1s spacing)

5f76fa07daebc0ead8b6d8657793bc336c452375 · 2026-06-10 16:35:52 -0700 · SteveStudio2

Files touched

Diff

commit 5f76fa07daebc0ead8b6d8657793bc336c452375
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed Jun 10 16:35:52 2026 -0700

    jf_fabrics recrawl: survive HTTP 429 throttle (retry+backoff, concurrency 1, 1.1s spacing)
---
 recrawl-jffabrics.js | 41 ++++++++++++++++++++++++++++++++---------
 1 file changed, 32 insertions(+), 9 deletions(-)

diff --git a/recrawl-jffabrics.js b/recrawl-jffabrics.js
index 1ed381d..b125def 100644
--- a/recrawl-jffabrics.js
+++ b/recrawl-jffabrics.js
@@ -50,7 +50,14 @@ function httpGet(url, maxRedirects = 4) {
         res.resume();
         return httpGet(loc, maxRedirects - 1).then(resolve).catch(reject);
       }
-      if (res.statusCode !== 200) { res.resume(); return reject(new Error('HTTP ' + res.statusCode)); }
+      if (res.statusCode !== 200) {
+        res.resume();
+        const err = new Error('HTTP ' + res.statusCode);
+        err.statusCode = res.statusCode;
+        const ra = res.headers['retry-after'];
+        if (ra) { const s = parseInt(ra, 10); if (!isNaN(s)) err.retryAfterMs = Math.min(120000, s * 1000); }
+        return reject(err);
+      }
       let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); res.on('error', reject);
     });
     req2.on('error', reject);
@@ -59,14 +66,28 @@ function httpGet(url, maxRedirects = 4) {
 }
 const sleep = (ms) => new Promise(r => setTimeout(r, ms + Math.floor(Math.random() * 250)));
 
-// Fetch with retry on transient short pages (the site throttles bursts).
+// Fetch with retry on transient short pages AND on HTTP 429 rate-limiting.
+// JF Fabrics enforces an IP burst quota that depletes after ~25 quick hits, then
+// returns 429 until it refills — so on 429 we back off (Retry-After if given,
+// else escalating cooldown) and retry, letting the single worker self-pace to a
+// sustainable rate rather than burning the whole worklist into errors.
 async function fetchPage(url) {
-  for (let attempt = 1; attempt <= 3; attempt++) {
-    const html = await httpGet(url);
-    if (html && html.length >= MIN_BYTES) return html;
-    await sleep(1500 * attempt);
+  for (let attempt = 1; attempt <= 6; attempt++) {
+    try {
+      const html = await httpGet(url);
+      if (html && html.length >= MIN_BYTES) { await sleep(0); return html; }
+      await sleep(1500 * attempt); // short page (throttle/challenge) — back off + retry
+    } catch (e) {
+      if (e.statusCode === 429) {
+        const wait = e.retryAfterMs || Math.min(90000, 4000 * attempt * attempt); // 4s,16s,36s,64s,90s,90s
+        await sleep(wait);
+        continue;
+      }
+      if (attempt >= 6) throw e;
+      await sleep(1000 * attempt);
+    }
   }
-  throw new Error('short-page (throttled) after 3 tries');
+  throw new Error('exhausted retries (throttled)');
 }
 
 (async () => {
@@ -81,7 +102,9 @@ async function fetchPage(url) {
 
   let pages = 0, skus = 0, withImg = 0, withSpecs = 0, errs = 0;
   let idx = 0;
-  const concurrency = 3;
+  // Single worker — JF rate-limits an IP burst; serial + backoff is the only
+  // way the full worklist completes instead of mostly 429-erroring.
+  const concurrency = 1;
   async function worker() {
     while (idx < rows.length) {
       const i = idx++;
@@ -101,7 +124,7 @@ async function fetchPage(url) {
         }
         pages++;
         if (pages % 25 === 0) console.log(`  ${pages}/${rows.length} | ${skus} skus | ${withImg} w/img | ${withSpecs} w/specs | ${errs} errs`);
-        await sleep(400);
+        await sleep(1100); // polite spacing to stay under the burst quota
       } catch (e) {
         errs++;
         if (errs % 10 === 0) console.error(`  err ${url}: ${e.message}`);

← 32a7723 jf_fabrics all_images fix: full-res extractor + additive rec  ·  back to Jf Fabrics Recrawl  ·  DWJJ MFR/spec backfill: staged 63-product JF scrape + dry-ru 6909b88 →