@@ -8,6 +8,11 @@ import {
8
8
type ILoadOptionsFunctions ,
9
9
type IRequestOptions ,
10
10
} from 'n8n-workflow' ;
11
+ import {
12
+ DEFAULT_EXP_BACKOFF_EXPONENTIAL ,
13
+ DEFAULT_EXP_BACKOFF_INTERVAL ,
14
+ DEFAULT_EXP_BACKOFF_RETRIES ,
15
+ } from '../helpers/consts' ;
11
16
12
17
type IApiRequestOptions = IRequestOptions & { uri ?: string } ;
13
18
@@ -50,7 +55,9 @@ export async function apiRequest(
50
55
) ;
51
56
}
52
57
53
- return await this . helpers . requestWithAuthentication . call ( this , authenticationMethod , options ) ;
58
+ return await retryWithExponentialBackoff ( ( ) =>
59
+ this . helpers . requestWithAuthentication . call ( this , authenticationMethod , options ) ,
60
+ ) ;
54
61
} catch ( error ) {
55
62
/**
56
63
* using `error instanceof NodeApiError` results in `false`
@@ -71,6 +78,60 @@ export async function apiRequest(
71
78
}
72
79
}
73
80
81
+ /**
82
+ * Checks if the given status code is retryable
83
+ * Status codes 429 (rate limit) and 500+ are retried,
84
+ * Other status codes 300-499 (except 429) are not retried,
85
+ * because the error is probably caused by invalid URL (redirect 3xx) or invalid user input (4xx).
86
+ */
87
+ function isStatusCodeRetryable ( statusCode : number ) {
88
+ if ( Number . isNaN ( statusCode ) ) return false ;
89
+
90
+ const RATE_LIMIT_EXCEEDED_STATUS_CODE = 429 ;
91
+ const isRateLimitError = statusCode === RATE_LIMIT_EXCEEDED_STATUS_CODE ;
92
+ const isInternalError = statusCode >= 500 ;
93
+ return isRateLimitError || isInternalError ;
94
+ }
95
+
96
+ /**
97
+ * Wraps a function with exponential backoff.
98
+ * If request fails with http code 500+ or doesn't return
99
+ * a code at all it is retried in 1s,2s,4s,.. up to maxRetries
100
+ * @param fn
101
+ * @param interval
102
+ * @param exponential
103
+ * @param maxRetries
104
+ * @returns
105
+ */
106
+ export async function retryWithExponentialBackoff (
107
+ fn : ( ) => Promise < any > ,
108
+ interval : number = DEFAULT_EXP_BACKOFF_INTERVAL ,
109
+ exponential : number = DEFAULT_EXP_BACKOFF_EXPONENTIAL ,
110
+ maxRetries : number = DEFAULT_EXP_BACKOFF_RETRIES ,
111
+ ) : Promise < any > {
112
+ let lastError ;
113
+ for ( let i = 0 ; i < maxRetries ; i ++ ) {
114
+ try {
115
+ return await fn ( ) ;
116
+ } catch ( error ) {
117
+ lastError = error ;
118
+ const status = Number ( error ?. httpCode ) ;
119
+ if ( isStatusCodeRetryable ( status ) ) {
120
+ //Generate a new sleep time based from interval * exponential^i function
121
+ const sleepTimeSecs = interval * Math . pow ( exponential , i ) ;
122
+ const sleepTimeMs = sleepTimeSecs * 1000 ;
123
+
124
+ await sleep ( sleepTimeMs ) ;
125
+
126
+ continue ;
127
+ }
128
+ throw error ;
129
+ }
130
+ }
131
+ //In case all of the calls failed with no status or isStatusCodeRetryable, throw the last error
132
+ throw lastError ;
133
+ }
134
+
74
135
export async function apiRequestAllItems (
75
136
this : IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions ,
76
137
requestOptions : IApiRequestOptions ,
0 commit comments