[object Object]

← back to Dw Signup Fulfillment

honor-reissue: 429-aware retry + gentle pacing for the 51-card burst run

5ea00345e64ec2e55c0aae392a5032b6386eeb62 · 2026-08-14 11:59:13 -0700 · steve

Files touched

Diff

commit 5ea00345e64ec2e55c0aae392a5032b6386eeb62
Author: steve <steve@designerwallcoverings.com>
Date:   Fri Aug 14 11:59:13 2026 -0700

    honor-reissue: 429-aware retry + gentle pacing for the 51-card burst run
---
 lib/shopify.js           | 30 ++++++++++++++++++++----------
 scripts/honor-reissue.js |  1 +
 2 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/lib/shopify.js b/lib/shopify.js
index 2c3c040..9863375 100644
--- a/lib/shopify.js
+++ b/lib/shopify.js
@@ -35,16 +35,26 @@ async function request(method, path, body) {
     return { ok: true, dryRun: true, noToken: true, method, url, body, status: 0, json: synthetic(path, body) };
   }
 
-  const res = await fetch(url, {
-    method,
-    headers: {
-      'X-Shopify-Access-Token': config.SHOPIFY_FULFILLMENT_TOKEN,
-      'Content-Type': 'application/json',
-      'Accept': 'application/json',
-    },
-    body: body !== undefined ? JSON.stringify(body) : undefined,
-  });
-  let json = null;
+  // Shopify Admin REST allows ~2 req/s (leaky bucket). A burst run (e.g. honoring 51 cards
+  // = ~150 calls) will hit 429; respect Retry-After and retry so the batch doesn't fail
+  // intermittently. Up to 5 attempts; non-429 errors return as-is for the caller to handle.
+  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+  let res, json = null;
+  for (let attempt = 0; attempt < 5; attempt++) {
+    res = await fetch(url, {
+      method,
+      headers: {
+        'X-Shopify-Access-Token': config.SHOPIFY_FULFILLMENT_TOKEN,
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+      },
+      body: body !== undefined ? JSON.stringify(body) : undefined,
+    });
+    if (res.status !== 429) break;
+    const wait = Math.max(1000, Math.round((parseFloat(res.headers.get('Retry-After')) || 2) * 1000));
+    log(`429 rate-limited on ${method} ${path} — retrying in ${wait}ms (attempt ${attempt + 1}/5)`);
+    await sleep(wait);
+  }
   try { json = await res.json(); } catch { json = null; }
   return { ok: res.ok, status: res.status, method, url, json };
 }
diff --git a/scripts/honor-reissue.js b/scripts/honor-reissue.js
index 300fed7..ee8588a 100644
--- a/scripts/honor-reissue.js
+++ b/scripts/honor-reissue.js
@@ -143,6 +143,7 @@ async function main() {
     // final ledger line reflecting delivery + void outcome
     appendLedger({ ...row, finalized: true });
     console.log(`  [${n}/${Math.min(pending.length, LIMIT)}] ${to.replace(/(.{2}).*@/, '$1***@')} → mint=${row.minted} email=${row.emailed} void=${row.voided} code=${newCode}`);
+    if (!config.DRY_RUN) await new Promise((r) => setTimeout(r, 350)); // gentle pace under Shopify's rate limit
   }
 
   console.log(`\nDONE (${config.DRY_RUN ? 'DRY_RUN' : 'LIVE'}): processed ${n} · minted ${minted} · emailed ${emailed} · voided ${voided}`);

← d5c8741 fix George Basic-auth resolution (real 401 root cause: wrong  ·  back to Dw Signup Fulfillment  ·  honor-reissue: isolate DRY ledger from live (dry preview no 28160cb →