@@ -253,16 +253,57 @@ function _doSingle (req) {
253253 }
254254}
255255
256+ /**
257+ * Helper to process a batch of 50 requests
258+ * @param { Array<GoogleAppsScript.URL_Fetch.URLFetchRequest> } fetchBatch
259+ * @param { Array<number> } batchIndices
260+ * @param { Map<number, RequestMapData> } requestMap
261+ * @param { Map<number, Object> } errorMap
262+ */
263+ function _processBatch ( fetchBatch , batchIndices , requestMap , errorMap ) {
264+ try {
265+ const responses = UrlFetchApp . fetchAll ( fetchBatch ) ;
266+ for ( let j = 0 ; j < responses . length ; j ++ ) {
267+ const originalIndex = batchIndices [ j ] ;
268+ requestMap . get ( originalIndex ) . response = responses [ j ] ;
269+ }
270+ } catch ( fetchAllErr ) {
271+ const fetchAllErrMsg = String ( fetchAllErr ) ;
272+ Logger . log ( `[WARN] fetchAll failed, retrying: ${ fetchAllErrMsg } ` ) ;
273+
274+ // Fallback: retry safe methods individually
275+ for ( let j = 0 ; j < fetchBatch . length ; j ++ ) {
276+ const originalIndex = batchIndices [ j ] ;
277+ const reqObj = requestMap . get ( originalIndex ) ;
278+
279+ if ( ! SAFE_REPLAY_METHODS . has ( reqObj . method ) ) {
280+ errorMap . set ( originalIndex , "batch fetchAll failed; unsafe method not replayed" ) ;
281+ continue ;
282+ }
283+
284+ try {
285+ const resp = UrlFetchApp . fetch ( reqObj . request . url , reqObj . request ) ;
286+ reqObj . response = resp ;
287+ } catch ( singleErr ) {
288+ const singleErrMsg = String ( singleErr ) ;
289+ Logger . log ( `[ERROR] _doBatch fallback[${ originalIndex } ]: ${ singleErrMsg } ` ) ;
290+ errorMap . set ( originalIndex , singleErrMsg ) ;
291+ }
292+ }
293+ }
294+ }
295+
256296/**
257297 * @param {Array<ClientRequest> } items
258298 * @returns {GoogleAppsScript.Content.TextOutput }
259299*/
260300function _doBatch ( items ) {
261- let fetchItems = [ ] ;
262- let fetchIndices = [ ] ;
263- let fetchMethods = [ ] ;
301+ /** @type { Map<number, {request: Object, method: string, response: GoogleAppsScript.URL_Fetch.HTTPResponse | null}> } */
302+ let requestMap = new Map ( ) ;
303+ /** @type { Map<number, string> } */
264304 let errorMap = new Map ( ) ;
265305
306+ // Build request map
266307 for ( let i = 0 ; i < items . length ; i ++ ) {
267308 let item = items [ i ] ;
268309 if ( ! item || typeof item !== "object" ) {
@@ -276,70 +317,78 @@ function _doBatch (items) {
276317 continue ;
277318 }
278319 try {
279- fetchItems . push ( { url : item . u , ... _buildOpts ( item ) } ) ;
280- fetchIndices . push ( i ) ;
281- fetchMethods . push ( ( item . m ?. toLowerCase ?. ( ) ?? "get" ) ) ;
320+ const method = ( item . m ?. toLowerCase ?. ( ) ?? "get" ) ;
321+ const request = { url : item . u , ... _buildOpts ( item ) } ;
322+ requestMap . set ( i , { request , method , response : null } ) ;
282323 } catch ( buildErr ) {
283324 const errMsg = String ( buildErr ) ;
284325 Logger . log ( `[ERROR] _doBatch[${ i } ] _buildOpts: ${ errMsg } ` ) ;
285326 errorMap . set ( i , errMsg ) ;
286327 }
287328 }
288329
289- // fetchAll() processes all requests in parallel inside Google. If it
290- // throws as a whole (e.g. one URL violates UrlFetchApp limits and
291- // poisons the whole batch), degrade to per-item fetch on safe methods
292- // so a single bad request does not zero out every response in the
293- // batch. Mirrors upstream `masterking32/MasterHttpRelayVPN@3094288`.
294- let responses = [ ] ;
295- try {
296- // Single - chunk fast path; avoids the fetchAll overhead for the common case.
297- if ( fetchItems . length === 1 ) {
298- responses = [ UrlFetchApp . fetch ( fetchItems [ 0 ] . url , fetchItems [ 0 ] ) ] ;
299- } else {
300- responses = UrlFetchApp . fetchAll ( fetchItems ) ;
330+ if ( requestMap . size === 0 ) {
331+ // All items failed validation
332+ let results = [ ] ;
333+ for ( let i = 0 ; i < items . length ; i ++ ) {
334+ results . push ( { e : String ( errorMap . get ( i ) ) } ) ;
301335 }
302- } catch ( fetchAllErr ) {
303- const fetchAllErrMsg = String ( fetchAllErr ) ;
304- responses = [ ] ;
305- for ( let j = 0 ; j < fetchItems . length ; j ++ ) {
306- try {
307- if ( ! SAFE_REPLAY_METHODS . has ( fetchMethods [ j ] ) ) {
308- errorMap . set ( fetchIndices [ j ] , "batch fetchAll failed; unsafe method not replayed" ) ;
309- responses [ j ] = null ;
310- continue ;
311- }
312- let fallbackReq = fetchItems [ j ] ;
313- responses [ j ] = UrlFetchApp . fetch ( fallbackReq . url , fallbackReq ) ;
314- } catch ( singleErr ) {
315- const singleErrMsg = String ( singleErr ) ;
316- Logger . log ( `[ERROR] _doBatch (fetching single item): ${ fetchIndices [ j ] } ` ) ;
317- errorMap . set ( fetchIndices [ j ] , singleErrMsg ) ;
318- responses [ j ] = null ;
336+ return _relayResponse ( { q : results } ) ;
337+ }
338+
339+ // Single-item fast path
340+ if ( requestMap . size === 1 ) {
341+ const [ originalIndex , data ] = requestMap . entries ( ) . next ( ) . value ;
342+ try {
343+ const resp = UrlFetchApp . fetch ( data . request . url , data . request ) ;
344+ data . response = resp ;
345+ } catch ( singleErr ) {
346+ const singleErrMsg = String ( singleErr ) ;
347+ Logger . log ( `[ERROR] _doBatch (fetching single item): ${ singleErrMsg } ` ) ;
348+ errorMap . set ( originalIndex , singleErrMsg ) ;
349+ }
350+
351+ } else {
352+ // Batch mode
353+ let requestCount = 0 ;
354+ let fetchBatch = [ ] ;
355+ let batchIndices = [ ] ;
356+
357+ for ( const [ originalIndex , data ] of requestMap ) {
358+ fetchBatch . push ( data . request ) ;
359+ batchIndices . push ( originalIndex ) ;
360+ requestCount ++ ;
361+
362+ // Process batch when it reaches 50 or we're at the end
363+ if ( fetchBatch . length === 50 || requestCount === requestMap . size ) {
364+ _processBatch ( fetchBatch , batchIndices , requestMap , errorMap ) ;
365+ fetchBatch = [ ] ;
366+ batchIndices = [ ] ;
319367 }
320368 }
321369 }
322370
371+ // Build results
372+ /** @type {Array<RelaySingleResponse | RelayErrorResponse> } */
323373 let results = [ ] ;
324374 for ( let i = 0 ; i < items . length ; i ++ ) {
325375 if ( errorMap . has ( i ) ) {
326376 results . push ( { e : String ( errorMap . get ( i ) ) } ) ;
327- } else {
328- const fetchPos = fetchIndices . indexOf ( i ) ;
329- if ( fetchPos === - 1 || ! responses [ fetchPos ] ) {
330- results . push ( { e : "fetch failed" } ) ;
331- } else {
332- try {
333- results . push ( _buildResponse ( responses [ fetchPos ] ) ) ;
334- } catch ( err ) {
335- results . push ( { e : `fetch failed: ${ String ( err ) } ` } ) ;
336- }
377+ } else if ( requestMap . has ( i ) && requestMap . get ( i ) . response ) {
378+ try {
379+ results . push ( _buildResponse ( requestMap . get ( i ) . response ) ) ;
380+ } catch ( err ) {
381+ results . push ( { e : `fetch failed: ${ String ( err ) } ` } ) ;
337382 }
383+ } else {
384+ results . push ( { e : "fetch failed" } ) ;
338385 }
339386 }
387+
340388 return _relayResponse ( { q : results } ) ;
341389}
342390
391+ /** @param {GoogleAppsScript.Events.DoPost } e */
343392function doPost ( e ) {
344393 if ( ! AUTH_KEY ) {
345394 Logger . log ( "[ERROR] doPost: AUTH_KEY not configured" ) ;
0 commit comments