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 ──────────────────────────────────────────