From f4f23c3173b15973578fa77db65d69b6f70143a6 Mon Sep 17 00:00:00 2001 From: dazzling-no-more <278675588+dazzling-no-more@users.noreply.github.com> Date: Tue, 12 May 2026 02:55:10 +0400 Subject: [PATCH] fix(code.gs): wrap _doSingle normal-relay fetch in try/catch (#1047, #1049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1047. `_doSingle`'s normal-relay path (cache disabled or cache miss on non-cachable request) ran `UrlFetchApp.fetch` → `getContent` → `base64Encode` with no error wrapper. Any throw — most commonly when the response body approaches Apps Script's ~50 MB ceiling and `base64Encode` blows the V8 heap, also URL-too-long / payload-too-large / quota exhaustion / 6-minute execution timeout — propagated unhandled, and Apps Script served its default `Web App` HTML error page in place of the JSON envelope. The Rust client (`parse_relay_json` in `domain_fronter.rs`) then failed to find JSON and surfaced the cryptic `bad response: no json in: ...` with no signal as to the actual cause. The reporter's symptom — a single failing host (`shc-dist.lostsig.co`, sonichacking.org) serving large ROM-hack binaries — matches this exactly. Every other download worked because they were all under the body-size ceiling. ## Fix Wrap the normal-relay block in `_doSingle` with `try { ... } catch (err) { return _json({ e: "fetch failed: " + String(err) }); }`. Mirrors the per-item try/catch already present in `_doBatch`. Turns the silent HTML crash into a structured `FronterError::Relay("fetch failed: …")` on the client side that pinpoints the real underlying error. Cache path intentionally untouched: - `_fetchAndCache` already wraps its own fetch in try/catch and returns `null` on any failure (so `_doSingle` falls through cleanly to the normal relay). - The cached-read path is bounded to ≤ `CACHE_MAX_BODY_BYTES` (35 KB) so it cannot trip the size limits that caused this bug. ## Verified locally on top of v1.9.22 - `node --check assets/apps_script/Code.gs`: clean ✅ - `cargo test --lib --release`: 209/209 ✅ (sanity — no Rust change) Reviewed via Anthropic Claude. Co-Authored-By: dazzling-no-more Co-Authored-By: Claude Opus 4.7 (1M context) --- assets/apps_script/Code.gs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/assets/apps_script/Code.gs b/assets/apps_script/Code.gs index 0a827f8..1392225 100644 --- a/assets/apps_script/Code.gs +++ b/assets/apps_script/Code.gs @@ -189,13 +189,27 @@ function _doSingle(req) { } // ── Normal relay (cache disabled or unavailable) ──────── - var opts = _buildOpts(req); - var resp = UrlFetchApp.fetch(req.u, opts); - return _json({ - s: resp.getResponseCode(), - h: _respHeaders(resp), - b: Utilities.base64Encode(resp.getContent()), - }); + // Wrap the fetch + body encode in try/catch so any failure surfaces as + // a JSON error envelope the Rust client can parse. Without this, throws + // from UrlFetchApp.fetch (URL too long, payload too large, quota + // exhausted, 6-minute execution timeout) or from base64Encode (response + // body near Apps Script's ~50 MB ceiling can blow the V8 heap during + // encode) propagate unhandled, and Apps Script serves its default + // `Web App` HTML error page — which the client then + // reports as "Relay failed: bad response: no json in: Web App>..." + // and the user has no signal as to the actual cause. Mirrors the + // per-item try/catch in _doBatch below. + try { + var opts = _buildOpts(req); + var resp = UrlFetchApp.fetch(req.u, opts); + return _json({ + s: resp.getResponseCode(), + h: _respHeaders(resp), + b: Utilities.base64Encode(resp.getContent()), + }); + } catch (err) { + return _json({ e: "fetch failed: " + String(err) }); + } } // ── Batch Request ──────────────────────────────────────────