← 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
M lib/shopify.jsM scripts/honor-reissue.js
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 →