[object Object]

← back to Rebel Walls Push

TK-10029: retry gql() on transient Shopify upstream errors (backoff)

c93997ddb048cef723495daa99a9bb0272e1352e · 2026-09-03 12:09:07 -0700 · Steve Abrams

A non-JSON 'upstream connect error' gateway blip was crashing long --apply
runs mid-flight (JSON.parse throw, no retry). Split into gqlOnce + a 6-attempt
exponential-backoff gql wrapper. Transport-only; rename set/hold logic unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit c93997ddb048cef723495daa99a9bb0272e1352e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 12:09:07 2026 -0700

    TK-10029: retry gql() on transient Shopify upstream errors (backoff)
    
    A non-JSON 'upstream connect error' gateway blip was crashing long --apply
    runs mid-flight (JSON.parse throw, no retry). Split into gqlOnce + a 6-attempt
    exponential-backoff gql wrapper. Transport-only; rename set/hold logic unchanged.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/relabel-units.js | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/scripts/relabel-units.js b/scripts/relabel-units.js
index b69caa6..80f6f05 100644
--- a/scripts/relabel-units.js
+++ b/scripts/relabel-units.js
@@ -65,7 +65,7 @@ function isWrongMuralLabel(value) {
   return false;
 }
 
-async function gql(query, vars) {
+async function gqlOnce(query, vars) {
   return new Promise((resolve, reject) => {
     const body = JSON.stringify({ query, variables: vars || {} });
     const req = https.request({
@@ -75,7 +75,7 @@ async function gql(query, vars) {
     }, res => {
       let d = '';
       res.on('data', c => d += c);
-      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { reject(e); } });
+      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { reject(new Error(`non-JSON response (${res.statusCode}): ${String(d).slice(0,80)}`)); } });
     });
     req.on('error', reject);
     req.write(body);
@@ -83,6 +83,24 @@ async function gql(query, vars) {
   });
 }
 
+// TK-10029: Shopify's gateway intermittently returns a non-JSON "upstream connect error"
+// (transient 502/503) that would otherwise crash a long apply run mid-flight. Retry transient
+// failures with exponential backoff; surface a permanent failure only after N attempts.
+async function gql(query, vars) {
+  let lastErr;
+  for (let attempt = 1; attempt <= 6; attempt++) {
+    try { return await gqlOnce(query, vars); }
+    catch (e) {
+      lastErr = e;
+      if (attempt === 6) break;
+      const backoff = Math.min(8000, 400 * 2 ** (attempt - 1));
+      console.log(`  [gql retry ${attempt}/5] ${e.message} — waiting ${backoff}ms`);
+      await sleep(backoff);
+    }
+  }
+  throw lastErr;
+}
+
 async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
 
 async function fetchAll() {

← dd06ad8 TK-10029: durable scope-B rollback map (1220 rows) + generat  ·  back to Rebel Walls Push  ·  TK-10029: runnable Scope-B rollback (reverse 1220 relabels f 12856c3 →