@@ -31,7 +31,7 @@ import {randomUUID} from 'crypto';
3131// @ts -ignore
3232import { getPackageJSON } from './package-json-helper.cjs' ;
3333import { GCCL_GCS_CMD_KEY } from './nodejs-common/util' ;
34- import { RetryOptions } from './storage' ;
34+ import { RETRYABLE_ERR_FN_DEFAULT , RetryOptions } from './storage' ;
3535
3636export interface StandardStorageQueryParams {
3737 alt ?: 'json' | 'media' ;
@@ -113,7 +113,11 @@ export class StorageTransport {
113113 }
114114 this . providedUserAgent = options . userAgent ;
115115 this . packageJson = getPackageJSON ( ) ;
116- this . retryOptions = options . retryOptions ;
116+ this . retryOptions = {
117+ ...options . retryOptions ,
118+ retryableErrorFn :
119+ options . retryOptions ?. retryableErrorFn || RETRYABLE_ERR_FN_DEFAULT ,
120+ } ;
117121 this . baseUrl = options . baseUrl ;
118122 this . timeout = options . timeout ;
119123 this . projectId = options . projectId ;
@@ -125,11 +129,6 @@ export class StorageTransport {
125129 callback ?: StorageTransportCallback < T > ,
126130 ) : Promise < void | T > {
127131 const headers = this . #buildRequestHeaders( reqOpts . headers ) ;
128- if ( reqOpts . headers ) {
129- Object . entries ( reqOpts . headers ) . forEach ( ( [ key , value ] ) => {
130- headers . set ( key , value as string ) ;
131- } ) ;
132- }
133132 if ( reqOpts [ GCCL_GCS_CMD_KEY ] ) {
134133 headers . set (
135134 'x-goog-api-client' ,
@@ -143,14 +142,6 @@ export class StorageTransport {
143142 }
144143 }
145144
146- const isDelete = reqOpts . method ?. toUpperCase ( ) === 'DELETE' ;
147- const urlString = reqOpts . url ?. toString ( ) || '' ;
148- const isAbsolute = urlString . startsWith ( 'http' ) ;
149- const isResumable =
150- urlString . includes ( 'uploadType=resumable' ) ||
151- urlString . includes ( '/upload/' ) ||
152- reqOpts . queryParameters ?. uploadType === 'resumable' ;
153-
154145 try {
155146 const getProjectId = async ( ) => {
156147 if ( reqOpts . projectId ) return reqOpts . projectId ;
@@ -166,246 +157,38 @@ export class StorageTransport {
166157 const requestPromise = this . authClient . request < T > ( {
167158 retryConfig : {
168159 retry : this . retryOptions . maxRetries ,
160+ statusCodesToRetry : [
161+ [ 408 , 408 ] ,
162+ [ 429 , 429 ] ,
163+ [ 500 , 500 ] ,
164+ [ 502 , 504 ] ,
165+ ] ,
166+ httpMethodsToRetry : [
167+ 'GET' ,
168+ 'HEAD' ,
169+ 'PUT' ,
170+ 'OPTIONS' ,
171+ 'DELETE' ,
172+ 'POST' ,
173+ ] ,
169174 noResponseRetries : this . retryOptions . maxRetries ,
170175 maxRetryDelay : this . retryOptions . maxRetryDelay ,
171176 retryDelayMultiplier : this . retryOptions . retryDelayMultiplier ,
177+ shouldRetry : this . retryOptions . retryableErrorFn ,
172178 totalTimeout : this . retryOptions . totalTimeout ,
173- shouldRetry : ( err : GaxiosError ) => {
174- const status = err . response ?. status ;
175- const errorCode = err . code ?. toString ( ) ;
176- const retryableStatuses = [ 408 , 429 , 500 , 502 , 503 , 504 ] ;
177- const nonRetryableStatuses = [ 401 , 405 , 412 ] ;
178-
179- const isMalformedResponse =
180- err . message ?. includes ( 'JSON' ) ||
181- err . message ?. includes ( 'Unexpected token <' ) ||
182- ( err . stack && err . stack . includes ( 'SyntaxError' ) ) ;
183- if ( isMalformedResponse ) return true ;
184-
185- if ( status && nonRetryableStatuses . includes ( status ) ) return false ;
186-
187- const params = reqOpts . queryParameters || { } ;
188- const hasPrecondition =
189- params . ifGenerationMatch !== undefined ||
190- params . ifMetagenerationMatch !== undefined ||
191- params . ifSourceGenerationMatch !== undefined ;
192-
193- const isPost = reqOpts . method ?. toUpperCase ( ) === 'POST' ;
194- const isPatch = reqOpts . method ?. toUpperCase ( ) === 'PATCH' ;
195- const isPut = reqOpts . method ?. toUpperCase ( ) === 'PUT' ;
196- const isGet = reqOpts . method ?. toUpperCase ( ) === 'GET' ;
197- const isHead = reqOpts . method ?. toUpperCase ( ) === 'HEAD' ;
198-
199- const isIam = urlString . includes ( '/iam' ) ;
200- const isAcl = urlString . includes ( '/acl' ) ;
201- const isHmacRequest = urlString . includes ( '/hmacKeys' ) ;
202- const isNotificationRequest = urlString . includes (
203- '/notificationConfigs' ,
204- ) ;
205-
206- // Logic for Mutations (POST, PATCH, DELETE)
207- if ( isPost || isPatch || isDelete ) {
208- const isRetryTest = urlString . includes ( 'retry-test-id' ) ;
209- if ( isPost && isAcl ) {
210- if ( isRetryTest ) {
211- return status ? retryableStatuses . includes ( status ) : false ;
212- }
213- return false ;
214- }
215- if ( isPost && ( isHmacRequest || isNotificationRequest ) )
216- return false ;
217-
218- const isBucketCreate =
219- isPost &&
220- urlString . includes ( '/v1/b' ) &&
221- ! urlString . includes ( '/o' ) ;
222- const isSafeDelete = isDelete && ! urlString . includes ( '/o/' ) ;
223-
224- if ( ! hasPrecondition ) {
225- if ( ! isBucketCreate && ! isSafeDelete ) {
226- if ( urlString . includes ( 'uploadType=resumable' ) && isPost ) {
227- return ! ! status && retryableStatuses . includes ( status ) ;
228- }
229- return false ;
230- }
231- }
232-
233- if ( status === undefined ) {
234- const isResumable = urlString . includes ( 'uploadType=resumable' ) ;
235-
236- if ( isResumable ) return false ;
237- return hasPrecondition || isBucketCreate || isSafeDelete ;
238- }
239-
240- return retryableStatuses . includes ( status ) ;
241- }
242-
243- if ( isPut ) {
244- const url = err . config ?. url . toString ( ) || '' ;
245- if ( isHmacRequest ) {
246- try {
247- const body =
248- typeof reqOpts . body === 'string'
249- ? JSON . parse ( reqOpts . body )
250- : reqOpts . body ;
251-
252- if ( ! body || ! body . etag ) {
253- return false ;
254- }
255- } catch ( e ) {
256- return false ;
257- }
258- } else if ( isIam ) {
259- try {
260- let hasIamPrecondition = false ;
261- const bodyStr =
262- typeof reqOpts . body === 'string'
263- ? reqOpts . body
264- : reqOpts . body instanceof Buffer
265- ? reqOpts . body . toString ( )
266- : '' ;
267- hasIamPrecondition = ! ! JSON . parse ( bodyStr || '{}' ) . etag ;
268- if ( ! hasIamPrecondition ) {
269- return false ;
270- }
271- return status === undefined || status === 503 ;
272- } catch ( e ) {
273- return false ;
274- }
275- } else if ( url . includes ( 'upload_id=' ) ) {
276- if ( ! status || retryableStatuses . includes ( status ) ) {
277- return true ;
278- }
279- return false ;
280- }
281- }
282-
283- // Logic for Idempotent Methods (GET, PUT, HEAD)
284- const isIdempotentMethod = isGet || isHead || isPut ;
285- if ( isIdempotentMethod ) {
286- if ( status === undefined ) {
287- return true ;
288- }
289- return retryableStatuses . includes ( status ) ;
290- }
291-
292- if (
293- isDelete &&
294- ! hasPrecondition &&
295- ! isNotificationRequest &&
296- ! isHmacRequest
297- )
298- return false ;
299-
300- const transientNetworkErrors = [
301- 'ECONNRESET' ,
302- 'ETIMEDOUT' ,
303- 'EADDRINUSE' ,
304- 'ECONNREFUSED' ,
305- 'EPIPE' ,
306- 'ENOTFOUND' ,
307- 'ENETUNREACH' ,
308- ] ;
309- if ( errorCode && transientNetworkErrors . includes ( errorCode ) )
310- return true ;
311-
312- const data = err . response ?. data ;
313- if ( data && data . error && Array . isArray ( data . error . errors ) ) {
314- for ( const e of data . error . errors ) {
315- const reason = e . reason ;
316- if (
317- reason === 'rateLimitExceeded' ||
318- reason === 'userRateLimitExceeded' ||
319- ( reason && reason . includes ( 'EAI_AGAIN' ) )
320- ) {
321- return true ;
322- }
323- }
324- }
325- if ( ! status ) return true ;
326- return status ? retryableStatuses . includes ( status ) : false ;
327- } ,
328179 } ,
329- params : isAbsolute ? undefined : reqOpts . queryParameters ,
330180 ...reqOpts ,
331181 headers,
332- url : isAbsolute
333- ? urlString
334- : this . #buildUrl( urlString , reqOpts . queryParameters ) ,
182+ url : this . #buildUrl( reqOpts . url ?. toString ( ) , reqOpts . queryParameters ) ,
335183 timeout : this . timeout ,
336- validateStatus : status =>
337- ( status >= 200 && status < 300 ) || ( isResumable && status === 308 ) ,
338- responseType :
339- isResumable || isDelete || reqOpts . responseType === 'text'
340- ? 'text'
341- : reqOpts . responseType === 'stream'
342- ? 'stream'
343- : 'json' ,
184+ validateStatus : status => status >= 200 && status < 300 ,
344185 } ) ;
345- const finalPromise = requestPromise
346- . then ( resp => {
347- let data = resp . data ;
348-
349- if (
350- data === undefined ||
351- data === null ||
352- ( typeof data === 'string' && data . trim ( ) === '' )
353- ) {
354- // eslint-disable-next-line @typescript-eslint/no-explicit-any
355- data = { } as any ;
356- }
357-
358- if ( data && typeof data === 'object' ) {
359- const plainHeaders : Record < string , string > = { } ;
360-
361- // eslint-disable-next-line @typescript-eslint/no-explicit-any
362- if (
363- resp . headers &&
364- // eslint-disable-next-line @typescript-eslint/no-explicit-any
365- typeof ( resp . headers as any ) . forEach === 'function'
366- ) {
367- // eslint-disable-next-line @typescript-eslint/no-explicit-any
368- ( resp . headers as any ) . forEach ( ( value : string , key : string ) => {
369- plainHeaders [ key . toLowerCase ( ) ] = value ;
370- } ) ;
371- } else if ( resp . headers ) {
372- // If headers is a plain object, normalize keys to lowercase
373- for ( const key of Object . keys ( resp . headers ) ) {
374- plainHeaders [ key . toLowerCase ( ) ] = (
375- resp . headers as unknown as Record < string , string >
376- ) [ key ] ;
377- }
378- }
379-
380- // eslint-disable-next-line @typescript-eslint/no-explicit-any
381- ( data as any ) . headers = plainHeaders ;
382- }
383186
384- if ( isDelete && ( data === '' || data === undefined ) ) {
385- data = { } as T ;
386- }
387- if ( callback ) {
388- callback ( null , data , resp ) ;
389- }
390- return data ;
391- } )
392- . catch ( error => {
393- const isMalformedResponse =
394- error . message ?. includes ( 'JSON' ) ||
395- ( error . cause &&
396- ( error . cause as Error ) . message ?. includes ( 'Unexpected token <' ) ) ||
397- ( error . stack && error . stack . includes ( 'SyntaxError' ) ) ;
398- if ( isMalformedResponse ) {
399- error . message = `Server returned non-JSON response: ${ error . response ?. status || 'unknown' } - ${ error . message } ` ;
400- } else if ( error . message ?. includes ( 'JSON' ) ) {
401- error . message = `Server returned non-JSON response: ${ error . response ?. status } ` ;
402- }
403- if ( callback ) {
404- callback ( error , null , error . response ) ;
405- }
406- throw error ;
407- } ) ;
408- return finalPromise ;
187+ return callback
188+ ? requestPromise
189+ . then ( resp => callback ( null , resp . data , resp ) )
190+ . catch ( err => callback ( err , null , err . response ) )
191+ : ( requestPromise . then ( resp => resp . data ) as Promise < T > ) ;
409192 } catch ( e ) {
410193 if ( callback ) return callback ( e as GaxiosError ) ;
411194 throw e ;
0 commit comments