← 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 →