From df1d69cf3caa332e6daf68c924a07043bf370942 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 3 Oct 2026 22:39:54 -0700 Subject: CORS write-off only when the host is reachable: a dropped connection is not a refusal Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR --- src/features/messages.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/features/messages.js b/src/features/messages.js index dc311d8..a517407 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -726,7 +726,8 @@ export function messagesFeature(ctx) { // thumbnail is to be had there, so after a second quick refusal the host // is left alone for a week rather than asked about every new face. // v2: the first list was mostly wrong (primal's CDN, coinos.io) — see corsFetch - const NOCORS_KEY = 'coinos-thumb-nocors-v2'; + // v3: v2 also caught dropped connections (primal again, on a phone) + const NOCORS_KEY = 'coinos-thumb-nocors-v3'; const NOCORS_MS = 7 * 24 * 3600_000; let noCors = null; // host -> { n, until } const noCorsNow = () => { if (!noCors) { try { noCors = JSON.parse(localStorage.getItem(NOCORS_KEY) || '{}'); } catch { noCors = {}; } } return noCors; }; @@ -747,12 +748,26 @@ export function messagesFeature(ctx) { // the host refused. That wrote primal and coinos.io off as "no CORS" and // left every picture hosted there painting from the full-size original. // So a refusal is retried once past the cache before anyone is blamed. + // And a refusal looks exactly like a dropped connection (a phone between + // networks, a DNS hiccup): both are a quick TypeError. Two of those wrote + // blossom.primal.net off for a week on a phone. So the host is only + // blamed (TypeError rethrown) when a plain no-cors ask of the same URL + // goes through; otherwise it's the network, and the error says so. async function corsFetch(url, signal) { try { return await fetch(url, { mode: 'cors', signal }); } catch (e) { if (!e || e.name !== 'TypeError') throw e; - return fetch(url, { mode: 'cors', cache: 'reload', signal }); + try { + return await fetch(url, { mode: 'cors', cache: 'reload', signal }); + } catch (e2) { + if (!e2 || e2.name !== 'TypeError') throw e2; + let reachable = false; + try { await fetch(url, { mode: 'no-cors', signal }); reachable = true; } catch {} + if (reachable) throw e2; + const net = new Error('network'); net.name = 'NetworkError'; + throw net; + } } } function makeThumb(pk, p) { -- cgit v1.2.3