@@ -3,7 +3,16 @@ import OAuthHandler from './oauthHandler'
33const defaultConfig = {
44 maxRequests : 5 ,
55 retryLimit : 5 ,
6- retryDelay : 300
6+ retryDelay : 300 ,
7+ // Enhanced retry configuration for transient network failures
8+ retryOnError : true ,
9+ retryOnNetworkFailure : true ,
10+ retryOnDnsFailure : true ,
11+ retryOnSocketFailure : true ,
12+ retryOnHttpServerError : true ,
13+ maxNetworkRetries : 3 ,
14+ networkRetryDelay : 100 , // Base delay for network retries (ms)
15+ networkBackoffStrategy : 'exponential' // 'exponential' or 'fixed'
716}
817
918export function ConcurrencyQueue ( { axios, config } ) {
@@ -19,13 +28,124 @@ export function ConcurrencyQueue ({ axios, config }) {
1928 } else if ( config . retryDelay && config . retryDelay < 300 ) {
2029 throw Error ( 'Retry Policy Error: minimum retry delay for requests is 300' )
2130 }
31+ // Validate network retry configuration
32+ if ( config . maxNetworkRetries && config . maxNetworkRetries < 0 ) {
33+ throw Error ( 'Network Retry Policy Error: maxNetworkRetries cannot be negative' )
34+ }
35+ if ( config . networkRetryDelay && config . networkRetryDelay < 50 ) {
36+ throw Error ( 'Network Retry Policy Error: minimum network retry delay is 50ms' )
37+ }
2238 }
2339
2440 this . config = Object . assign ( { } , defaultConfig , config )
2541 this . queue = [ ]
2642 this . running = [ ]
2743 this . paused = false
2844
45+ // Helper function to determine if an error is a transient network failure
46+ const isTransientNetworkError = ( error ) => {
47+ // DNS resolution failures
48+ if ( this . config . retryOnDnsFailure && error . code === 'EAI_AGAIN' ) {
49+ return { type : 'DNS_RESOLUTION' , reason : 'DNS resolution failure (EAI_AGAIN)' }
50+ }
51+
52+ // Socket and connection errors
53+ if ( this . config . retryOnSocketFailure ) {
54+ const socketErrorCodes = [ 'ECONNRESET' , 'ETIMEDOUT' , 'ECONNREFUSED' , 'ENOTFOUND' , 'EHOSTUNREACH' ]
55+ if ( socketErrorCodes . includes ( error . code ) ) {
56+ return { type : 'SOCKET_ERROR' , reason : `Socket error: ${ error . code } ` }
57+ }
58+ }
59+
60+ // Connection timeouts
61+ if ( this . config . retryOnNetworkFailure && error . code === 'ECONNABORTED' ) {
62+ return { type : 'TIMEOUT' , reason : 'Connection timeout' }
63+ }
64+
65+ // HTTP 5xx server errors
66+ if ( this . config . retryOnHttpServerError && error . response && error . response . status >= 500 && error . response . status <= 599 ) {
67+ return { type : 'HTTP_SERVER_ERROR' , reason : `HTTP ${ error . response . status } server error` }
68+ }
69+
70+ return null
71+ }
72+
73+ // Calculate retry delay with backoff strategy
74+ const calculateNetworkRetryDelay = ( attempt ) => {
75+ const baseDelay = this . config . networkRetryDelay
76+ if ( this . config . networkBackoffStrategy === 'exponential' ) {
77+ return baseDelay * Math . pow ( 2 , attempt - 1 )
78+ }
79+ return baseDelay // Fixed delay
80+ }
81+
82+ // Log retry attempts
83+ const logRetryAttempt = ( errorInfo , attempt , delay ) => {
84+ const message = `Transient ${ errorInfo . type } detected: ${ errorInfo . reason } . Retry attempt ${ attempt } /${ this . config . maxNetworkRetries } in ${ delay } ms`
85+ if ( this . config . logHandler ) {
86+ this . config . logHandler ( 'warning' , message )
87+ } else {
88+ console . warn ( `[Contentstack SDK] ${ message } ` )
89+ }
90+ }
91+
92+ // Log final failure
93+ const logFinalFailure = ( errorInfo , maxRetries ) => {
94+ const message = `Final retry failed for ${ errorInfo . type } : ${ errorInfo . reason } . Exceeded max retries (${ maxRetries } ).`
95+ if ( this . config . logHandler ) {
96+ this . config . logHandler ( 'error' , message )
97+ } else {
98+ console . error ( `[Contentstack SDK] ${ message } ` )
99+ }
100+ }
101+
102+ // Enhanced retry function for network errors
103+ const retryNetworkError = ( error , errorInfo , attempt ) => {
104+ if ( attempt > this . config . maxNetworkRetries ) {
105+ logFinalFailure ( errorInfo , this . config . maxNetworkRetries )
106+ // Final error message
107+ const finalError = new Error ( `Network request failed after ${ this . config . maxNetworkRetries } retries: ${ errorInfo . reason } ` )
108+ finalError . code = error . code
109+ finalError . originalError = error
110+ finalError . retryAttempts = attempt - 1
111+ return Promise . reject ( finalError )
112+ }
113+
114+ const delay = calculateNetworkRetryDelay ( attempt )
115+ logRetryAttempt ( errorInfo , attempt , delay )
116+
117+ // Initialize retry count if not present
118+ if ( ! error . config . networkRetryCount ) {
119+ error . config . networkRetryCount = 0
120+ }
121+ error . config . networkRetryCount = attempt
122+
123+ return new Promise ( ( resolve , reject ) => {
124+ setTimeout ( ( ) => {
125+ // Remove the failed request from running queue
126+ const runningIndex = this . running . findIndex ( item => item . request === error . config )
127+ if ( runningIndex !== - 1 ) {
128+ this . running . splice ( runningIndex , 1 )
129+ }
130+
131+ // Retry the request
132+ axios ( updateRequestConfig ( error , `Network retry ${ attempt } ` , delay ) )
133+ . then ( resolve )
134+ . catch ( ( retryError ) => {
135+ // Check if this is still a transient error and we can retry again
136+ const retryErrorInfo = isTransientNetworkError ( retryError )
137+ if ( retryErrorInfo ) {
138+ retryNetworkError ( retryError , retryErrorInfo , attempt + 1 )
139+ . then ( resolve )
140+ . catch ( reject )
141+ } else {
142+ reject ( retryError )
143+ }
144+ } )
145+ } , delay )
146+ } )
147+ }
148+
29149 // Initial shift will check running request,
30150 // and adds request to running queue if max requests are not running
31151 this . initialShift = ( ) => {
@@ -226,12 +346,20 @@ export function ConcurrencyQueue ({ axios, config }) {
226346 const responseErrorHandler = error => {
227347 let networkError = error . config . retryCount
228348 let retryErrorType = null
349+
350+ // First, check for transient network errors
351+ const networkErrorInfo = isTransientNetworkError ( error )
352+ if ( networkErrorInfo && this . config . retryOnNetworkFailure ) {
353+ const networkRetryCount = error . config . networkRetryCount || 0
354+ return retryNetworkError ( error , networkErrorInfo , networkRetryCount + 1 )
355+ }
356+
357+ // Original retry logic for non-network errors
229358 if ( ! this . config . retryOnError || networkError > this . config . retryLimit ) {
230359 return Promise . reject ( responseHandler ( error ) )
231360 }
232- // Check rate limit remaining header before retrying
233361
234- // Error handling
362+ // Check rate limit remaining header before retrying
235363 const wait = this . config . retryDelay
236364 var response = error . response
237365 if ( ! response ) {
@@ -300,7 +428,12 @@ export function ConcurrencyQueue ({ axios, config }) {
300428
301429 const updateRequestConfig = ( error , retryErrorType , wait ) => {
302430 const requestConfig = error . config
303- this . config . logHandler ( 'warning' , `${ retryErrorType } error occurred. Waiting for ${ wait } ms before retrying...` )
431+ const message = `${ retryErrorType } error occurred. Waiting for ${ wait } ms before retrying...`
432+ if ( this . config . logHandler ) {
433+ this . config . logHandler ( 'warning' , message )
434+ } else {
435+ console . warn ( `[Contentstack SDK] ${ message } ` )
436+ }
304437 if ( axios !== undefined && axios . defaults !== undefined ) {
305438 if ( axios . defaults . agent === requestConfig . agent ) {
306439 delete requestConfig . agent
0 commit comments