[object Object]

← back to Quadrille Showroom

ContrarianGo iter1: shared fetchUpstreamBuffered for BOTH thumb + proxy; split transient/permanent; warn-log

97b809a595b702cb583d9cccbbe49a7762bb55f2 · 2026-07-02 09:28:37 -0700 · Steve

Closes the 3 contrarian holes in the 5x sliver-thumb fix:
- /api/proxy/image (open hero) now shares the SAME cap+retry hardening as /api/thumb
  via one fetchUpstreamBuffered() helper — no more fixing only the tested endpoint.
- fallback distinguishes transient (blip) from PERMANENT (dead 4xx) and console.warns
  every fallback so a dead upstream leaves a trail a canary can grep — not a silent gray lie.
- both routes de-duplicated onto the shared helper (the twin functions collapsed).
Verified: healthy hero serves real 124KB image (no fallback header); permanent 404 →
placeholder 200 + logged PERMANENT; clickthrough CLEAN twice cold; verify-all 0 failures.
Adds /ContrarianGo skill (this loop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 97b809a595b702cb583d9cccbbe49a7762bb55f2
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 2 09:28:37 2026 -0700

    ContrarianGo iter1: shared fetchUpstreamBuffered for BOTH thumb + proxy; split transient/permanent; warn-log
    
    Closes the 3 contrarian holes in the 5x sliver-thumb fix:
    - /api/proxy/image (open hero) now shares the SAME cap+retry hardening as /api/thumb
      via one fetchUpstreamBuffered() helper — no more fixing only the tested endpoint.
    - fallback distinguishes transient (blip) from PERMANENT (dead 4xx) and console.warns
      every fallback so a dead upstream leaves a trail a canary can grep — not a silent gray lie.
    - both routes de-duplicated onto the shared helper (the twin functions collapsed).
    Verified: healthy hero serves real 124KB image (no fallback header); permanent 404 →
    placeholder 200 + logged PERMANENT; clickthrough CLEAN twice cold; verify-all 0 failures.
    Adds /ContrarianGo skill (this loop).
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 server.js | 87 +++++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 51 insertions(+), 36 deletions(-)

diff --git a/server.js b/server.js
index 8dcdb27..265539a 100644
--- a/server.js
+++ b/server.js
@@ -223,7 +223,7 @@ const ALLOWED_HOSTS = new Set([
   'www.designerwallcoverings.com',
   'designerwallcoverings.com',
 ]);
-app.get('/api/proxy/image', (req, res) => {
+app.get('/api/proxy/image', async (req, res) => {
   const url = req.query.url;
   if (!url) return res.status(400).send('Missing url');
   let parsed;
@@ -232,15 +232,18 @@ app.get('/api/proxy/image', (req, res) => {
     return res.status(403).send('Host not allowed');
   }
   try {
-    const https = require('https');
-    https.get(url, (proxyRes) => {
-      if (proxyRes.statusCode >= 400) { res.status(proxyRes.statusCode).end(); proxyRes.resume(); return; }
-      res.setHeader('Content-Type', proxyRes.headers['content-type'] || 'image/jpeg');
-      res.setHeader('Cache-Control', 'public, max-age=86400');
-      proxyRes.pipe(res);
-    }).on('error', () => res.status(502).send('Proxy error'));
+    const { buf, contentType } = await fetchUpstreamBuffered(url);   // shared cap+retry path
+    res.setHeader('Content-Type', contentType || 'image/jpeg');
+    res.setHeader('Cache-Control', 'public, max-age=86400');
+    return res.end(buf);
   } catch (e) {
-    res.status(500).send('Error');
+    // Never silently mask — LOG every fallback so a dead upstream leaves a trail a canary can
+    // grep (permanent 4xx flagged distinctly from a transient blip). The hero still degrades
+    // to a quiet placeholder rather than a broken-image icon + console 502.
+    const permanent = e.status && e.status < 500;
+    console.warn(`[proxy] upstream ${permanent ? 'PERMANENT ' + e.status : 'transient'} for ${url} — ${e.message}`);
+    if (sharp) { try { res.setHeader('X-Proxy-Fallback', '1'); return res.end(await placeholderThumb(512)); } catch (e2) {} }
+    return res.status(502).send('Proxy error');
   }
 });
 
@@ -287,6 +290,39 @@ async function placeholderThumb(w) {
   return buf;
 }
 
+// SHARED upstream image fetch — the single reliability path for BOTH /api/proxy/image (open
+// hero) and /api/thumb (closed slivers). Whitelist is checked by the caller. Buffered so the
+// concurrency cap + retry apply uniformly (hero heroes are ~125KB; buffering is cheap and
+// lets both routes share the same hardening — no more "fixed the tested endpoint, left its
+// twin bleeding"). Retries TRANSIENT only (network err / 5xx / timeout); a real 4xx (dead
+// object) throws immediately with .status set so the caller can log it as PERMANENT, not
+// silently mask it. Returns { buf, contentType }.
+function fetchUpstreamBuffered(url, { tries = 3, timeoutMs = 8000 } = {}) {
+  const https = require('https');
+  const once = (u) => new Promise((resolve, reject) => {
+    const req = https.get(u, (pr) => {
+      if (pr.statusCode >= 400) { reject(Object.assign(new Error('status ' + pr.statusCode), { status: pr.statusCode })); pr.resume(); return; }
+      const chunks = []; pr.on('data', c => chunks.push(c));
+      pr.on('end', () => resolve({ buf: Buffer.concat(chunks), contentType: pr.headers['content-type'] }));
+    });
+    req.on('error', reject);
+    req.setTimeout(timeoutMs, () => req.destroy(new Error('upstream timeout')));
+  });
+  return (async () => {
+    await acquireUpstream();
+    try {
+      for (let attempt = 0; ; attempt++) {
+        try { return await once(url); }
+        catch (e) {
+          const transient = !e.status || e.status >= 500;
+          if (attempt >= tries - 1 || !transient) throw e;
+          await new Promise(r => setTimeout(r, 250 * (attempt + 1)));
+        }
+      }
+    } finally { releaseUpstream(); }
+  })();
+}
+
 function sendThumb(res, buf, key) {
   res.setHeader('Content-Type', 'image/jpeg');
   res.setHeader('Cache-Control', 'public, max-age=604800');
@@ -329,36 +365,15 @@ app.get('/api/thumb', async (req, res) => {
       let parsed;
       try { parsed = new URL(url); } catch { return res.status(400).send('Bad url'); }
       if (parsed.protocol !== 'https:' || !ALLOWED_HOSTS.has(parsed.hostname)) return res.status(403).send('Host not allowed');
-      const https = require('https');
-      const fetchBuf = (u) => new Promise((resolve, reject) => {
-        const req = https.get(u, (pr) => {
-          if (pr.statusCode >= 400) { reject(Object.assign(new Error('status ' + pr.statusCode), { status: pr.statusCode })); pr.resume(); return; }
-          const chunks = []; pr.on('data', c => chunks.push(c)); pr.on('end', () => resolve(Buffer.concat(chunks)));
-        });
-        req.on('error', reject);
-        req.setTimeout(8000, () => req.destroy(new Error('upstream timeout')));
-      });
-      // Retry w/ exponential backoff behind a concurrency cap — a cold-cache burst of ~24
-      // sliver thumbs can trip a TRANSIENT upstream hiccup (5xx/timeout) that shouldn't
-      // surface as a hard 502. The cap (acquireUpstream) prevents the burst; retries mop up
-      // any straggler. Never retry a real 4xx (dead URL) — only transient network errors/5xx.
       try {
-        await acquireUpstream();
-        let buf;
-        try {
-          for (let attempt = 0; ; attempt++) {
-            try { buf = await fetchBuf(url); break; }
-            catch (e) {
-              const transient = !e.status || e.status >= 500;
-              if (attempt >= 2 || !transient) throw e;
-              await new Promise(r => setTimeout(r, 250 * (attempt + 1)));
-            }
-          }
-        } finally { releaseUpstream(); }
+        const { buf } = await fetchUpstreamBuffered(url);   // shared cap+retry path (same as proxy)
         return sendThumb(res, await downscale(buf));
       } catch (e) {
-        // Transient upstream fetch exhausted (or a dead object) — serve a quiet placeholder
-        // at 200 so a decorative closed sliver never surfaces a console 502. Fail-safe.
+        // Never silently mask — LOG the fallback (permanent 4xx flagged distinctly) so a dead
+        // sliver URL leaves a trail. Decorative closed sliver still degrades to a quiet 200
+        // placeholder rather than a console 502.
+        const permanent = e.status && e.status < 500;
+        console.warn(`[thumb] upstream ${permanent ? 'PERMANENT ' + e.status : 'transient'} for ${url} — ${e.message}`);
         res.setHeader('X-Thumb-Fallback', '1');
         return sendThumb(res, await placeholderThumb(w));
       }

← b6f8daf 5x sweeps 2-5: upstream concurrency cap + placeholder fail-s  ·  back to Quadrille Showroom  ·  chore: v1.3.1 (session close — thumb/proxy upstream hardenin 20c04ec →