← Commit history

POST retry-once + honest banner gating

John Lauer ·c7acfb3852 ·2mo ago ·parent 275bdc7
1 file changed +23−8
web-control-demo.html+23−8
@@ -177,10 +177,25 @@ for (const w of mine) {   var SANDBOXED = !location.origin || location.origin === "null";
   function sandboxWarn() { document.getElementById("sandbox-warn").style.display = "block"; }
   if (SANDBOXED) sandboxWarn();
-  // Any fetch that dies with the browser's opaque TypeError (CSP block, no
-  // listener, mixed-content) escalates the banner too - the single most
-  // common cause is the sandboxed-embed case above.
-  function fetchDied(e) { if (/failed to fetch|networkerror/i.test(String(e))) sandboxWarn(); }
+  // A dead fetch means the SANDBOX banner ONLY when we actually are sandboxed.
+  // In a real tab the same TypeError is a network transient (ab restarting, a
+  // stale keep-alive socket after a reboot) - showing the null-origin banner
+  // there was a lie John caught on 2026-08-17. Real-tab failures are handled
+  // by the one-shot POST retry in postRaw below.
+  function fetchDied(e) { if (SANDBOXED && /failed to fetch|networkerror/i.test(String(e))) sandboxWarn(); }
+  // Chrome auto-retries a GET on a stale reused connection but NEVER a POST -
+  // so one dead keep-alive socket makes exactly one button fail instantly.
+  // Retry a network-failed POST once after 600ms; a repeat failure is real.
+  function postRaw(body) {
+    var doFetch = function () {
+      return fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
+    };
+    return doFetch().catch(function (e) {
+      if (SANDBOXED || !/failed to fetch|networkerror/i.test(String(e))) throw e;
+      logLine("  (network drop - retrying the POST once)");
+      return new Promise(function (r) { setTimeout(r, 600); }).then(doFetch);
+    });
+  }
   function logLine(m) { log.textContent += "\n" + m; log.scrollTop = log.scrollHeight; }
   function chosenTarget() { return sel.value || null; }
   // WHO is "this machine": ab 2.0.53+ reports its relay client name at
@@ -253,9 +268,9 @@ for (const w of mine) {     if (target === undefined) target = chosenTarget(); if (target) body.target = target;
     if (target) { body.args = body.args || {}; if (!body.args.caller) body.args.caller = { aiThread: "web-control demo page", containerName: selfLabel() }; }
     var who = machineLabel(target || null);
-    setRes(el, "pending", "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast");
+    setRes(el, "pending", "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast (no banner? Windows may have tucked it in the notification center - press Win+N)");
     logLine("POST /command " + JSON.stringify(body) + "   [on " + who + "]");
-    fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
+    postRaw(body)
       .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })
       .then(function (res) {
         logLine("  <- HTTP " + res.s + " " + JSON.stringify(res.j).slice(0, 300));
@@ -263,7 +278,7 @@ for (const w of mine) {         else if (res.j && res.j.errorCode === "web_control_disabled") setRes(el, "err", "Refused: web control OFF", "re-enable in Settings &rarr; Web Control");
         else setRes(el, "err", "Not approved", ((res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s)) + " - missed the prompt? The toast waits in your notification center for 1 hour; Allow 1 hr there, then click again");
       })
-      .catch(function (e) { fetchDied(e); setRes(el, "err", "blocked", SANDBOXED ? "sandboxed embed blocks loopback - open http://127.0.0.1:24681 directly (see banner)" : String(e)); });
+      .catch(function (e) { fetchDied(e); setRes(el, "err", "blocked", SANDBOXED ? "sandboxed embed blocks loopback - open http://127.0.0.1:24681 directly (see banner)" : "network drop to ab (stale connection or ab restarting) - retried once; click again in a moment. " + String(e)); });
   }
   // POST that returns the flattened JSON. Optional target.
   function postVerb(command, args, target) {
@@ -272,7 +287,7 @@ for (const w of mine) {     // caller identity - name this page as the thread so the run is attributed.
     if (target) { body.args = body.args || {}; if (!body.args.caller) body.args.caller = { aiThread: "web-control demo page", containerName: selfLabel() }; }
     logLine("POST /command " + JSON.stringify(body));
-    return fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
+    return postRaw(body)
       .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: flat(j) }; }); });
   }
   // Loopback self POST (the window array + per-window screenshots run on THIS machine).