@@ -161,6 +161,9 @@ function _doSingle(req) {
161161 return _json ( { e : "bad url" } ) ;
162162 }
163163
164+ // ── Optional cache path ────────────────────────────────
165+ // Only entered when CACHE_SPREADSHEET_ID is configured and
166+ // the request qualifies as a public, cachable GET.
164167 if ( _canUseCache ( req ) ) {
165168 var cached = _getFromCache ( req . u , req . h ) ;
166169 if ( cached ) {
@@ -181,25 +184,41 @@ function _doSingle(req) {
181184 cached : false ,
182185 } ) ;
183186 }
184- // _fetchAndCache returned null → fall through to normal relay
185- }
186-
187- var opts = _buildOpts ( req ) ;
188- var resp = UrlFetchApp . fetch ( req . u , opts ) ;
187+ // If _fetchAndCache returns null (spreadsheet unavailable),
188+ // fall through to the normal relay path below.
189+ }
190+
191+ // ── Normal relay (cache disabled or unavailable) ────────
192+ // Wrap the fetch + body encode in try/catch so any failure surfaces as
193+ // a JSON error envelope the Rust client can parse. Without this, throws
194+ // from UrlFetchApp.fetch (URL too long, payload too large, quota
195+ // exhausted, 6-minute execution timeout) or from base64Encode (response
196+ // body near Apps Script's ~50 MB ceiling can blow the V8 heap during
197+ // encode) propagate unhandled, and Apps Script serves its default
198+ // `<title>Web App</title>` HTML error page — which the client then
199+ // reports as "Relay failed: bad response: no json in: <title>Web App>..."
200+ // and the user has no signal as to the actual cause. Mirrors the
201+ // per-item try/catch in _doBatch below.
202+ try {
203+ var opts = _buildOpts ( req ) ;
204+ var resp = UrlFetchApp . fetch ( req . u , opts ) ;
205+
206+ // Raw-return mode for exit-node path.
207+ // r:true = return destination body verbatim so Rust gets {s,h,b} unwrapped.
208+ if ( req . r === true ) {
209+ return ContentService
210+ . createTextOutput ( resp . getContentText ( ) )
211+ . setMimeType ( ContentService . MimeType . JSON ) ;
212+ }
189213
190- // Raw-return mode for exit-node path.
191- // r:true = return destination body verbatim so Rust gets {s,h,b} unwrapped.
192- if ( req . r === true ) {
193- return ContentService
194- . createTextOutput ( resp . getContentText ( ) )
195- . setMimeType ( ContentService . MimeType . JSON ) ;
214+ return _json ( {
215+ s : resp . getResponseCode ( ) ,
216+ h : _respHeaders ( resp ) ,
217+ b : Utilities . base64Encode ( resp . getContent ( ) ) ,
218+ } ) ;
219+ } catch ( err ) {
220+ return _json ( { e : "fetch failed: " + String ( err ) } ) ;
196221 }
197-
198- return _json ( {
199- s : resp . getResponseCode ( ) ,
200- h : _respHeaders ( resp ) ,
201- b : Utilities . base64Encode ( resp . getContent ( ) ) ,
202- } ) ;
203222}
204223
205224// ── Batch Request ──────────────────────────────────────────
0 commit comments