Skip to content

Commit 04bc664

Browse files
Merge pull request #119 from contentstack/development
DX | 18-08-2025 | Staging
2 parents b9c2a28 + ea7a02f commit 04bc664

File tree

4 files changed

+189
-3
lines changed

4 files changed

+189
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
## Change log
2+
### Version: 1.2.4
3+
#### Date: Aug-18-2025
4+
- Fix: Retry request logic after rate limit replenishes
5+
26
### Version: 1.2.3
37
#### Date: Aug-04-2025
48
- Fix: Added Pre-commit hook to run talisman and snyk scan

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/core",
3-
"version": "1.2.3",
3+
"version": "1.2.4",
44
"type": "commonjs",
55
"main": "./dist/cjs/src/index.js",
66
"types": "./dist/cjs/src/index.d.ts",

src/lib/retryPolicy/delivery-sdk-handlers.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,30 @@ export const retryResponseErrorHandler = (error: any, config: any, axiosInstance
4545
}
4646
} else {
4747
const rateLimitRemaining = response.headers['x-ratelimit-remaining'];
48+
49+
// Handle rate limit exhaustion with retry logic
4850
if (rateLimitRemaining !== undefined && parseInt(rateLimitRemaining) <= 0) {
49-
return Promise.reject(error.response.data);
51+
retryCount++;
52+
53+
if (retryCount >= config.retryLimit) {
54+
return Promise.reject(error.response.data);
55+
}
56+
57+
error.config.retryCount = retryCount;
58+
59+
// Calculate delay for rate limit reset
60+
const rateLimitResetDelay = calculateRateLimitDelay(response.headers);
61+
62+
return new Promise((resolve, reject) => {
63+
setTimeout(async () => {
64+
try {
65+
const retryResponse = await axiosInstance(error.config);
66+
resolve(retryResponse);
67+
} catch (retryError) {
68+
reject(retryError);
69+
}
70+
}, rateLimitResetDelay);
71+
});
5072
}
5173

5274
if (response.status == 429 || response.status == 401) {
@@ -99,3 +121,52 @@ const retry = (error: any, config: any, retryCount: number, retryDelay: number,
99121
}, delayTime);
100122
});
101123
};
124+
125+
/**
126+
* Calculate delay time for rate limit reset based on response headers
127+
* @param headers - Response headers from the API
128+
* @returns Delay time in milliseconds
129+
*/
130+
const calculateRateLimitDelay = (headers: any): number => {
131+
// Check for retry-after header (in seconds)
132+
const retryAfter = headers['retry-after'];
133+
if (retryAfter) {
134+
return parseInt(retryAfter) * 1000; // Convert to milliseconds
135+
}
136+
137+
// Check for x-ratelimit-reset header (Unix timestamp)
138+
const rateLimitReset = headers['x-ratelimit-reset'];
139+
if (rateLimitReset) {
140+
const resetTime = parseInt(rateLimitReset) * 1000; // Convert to milliseconds
141+
const currentTime = Date.now();
142+
const delay = resetTime - currentTime;
143+
144+
// Ensure we have a positive delay, add a small buffer
145+
return Math.max(delay + 1000, 1000); // At least 1 second delay
146+
}
147+
148+
// Check for x-ratelimit-reset-time header (ISO string)
149+
const rateLimitResetTime = headers['x-ratelimit-reset-time'];
150+
if (rateLimitResetTime) {
151+
const resetTime = new Date(rateLimitResetTime).getTime();
152+
const currentTime = Date.now();
153+
const delay = resetTime - currentTime;
154+
155+
// Ensure we have a positive delay, add a small buffer
156+
return Math.max(delay + 1000, 1000); // At least 1 second delay
157+
}
158+
159+
// Default fallback delay (60 seconds) if no rate limit reset info is available
160+
return 60000;
161+
};
162+
163+
/**
164+
* Retry request after specified delay
165+
* @param error - The original error object
166+
* @param delay - Delay time in milliseconds
167+
* @param axiosInstance - Axios instance to retry with
168+
* @returns Promise that resolves after the delay and retry
169+
*/
170+
const retryWithDelay = async (error: any, delay: number, axiosInstance: AxiosInstance) => {
171+
return
172+
};

test/retryPolicy/delivery-sdk-handlers.spec.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,120 @@ describe('retryResponseErrorHandler', () => {
251251
expect(retryCondition).toHaveBeenCalledWith(error);
252252
});
253253

254-
it('should reject with rate limit error when x-ratelimit-remaining is 0', async () => {
254+
it('should retry with delay when x-ratelimit-remaining is 0 and retry-after header is present', async () => {
255255
const error = {
256256
config: { retryOnError: true, retryCount: 1 },
257+
response: {
258+
status: 429,
259+
headers: {
260+
'x-ratelimit-remaining': '0',
261+
'retry-after': '1', // 1 second for faster testing
262+
},
263+
data: {
264+
error_message: 'Rate limit exceeded',
265+
error_code: 429,
266+
errors: null,
267+
},
268+
},
269+
};
270+
const config = { retryLimit: 3 };
271+
const client = axios.create();
272+
273+
// Mock successful response after retry
274+
mock.onAny().reply(200, { success: true });
275+
276+
jest.useFakeTimers();
277+
278+
const responsePromise = retryResponseErrorHandler(error, config, client);
279+
280+
// Fast-forward time by 1 second
281+
jest.advanceTimersByTime(1000);
282+
283+
const response: any = await responsePromise;
284+
285+
expect(response.status).toBe(200);
286+
expect(response.data.success).toBe(true);
287+
288+
jest.useRealTimers();
289+
});
290+
291+
it('should retry with delay when x-ratelimit-remaining is 0 and x-ratelimit-reset header is present', async () => {
292+
const error = {
293+
config: { retryOnError: true, retryCount: 1 },
294+
response: {
295+
status: 429,
296+
headers: {
297+
'x-ratelimit-remaining': '0',
298+
'x-ratelimit-reset': Math.floor((Date.now() + 2000) / 1000).toString(), // 2 seconds from now
299+
},
300+
data: {
301+
error_message: 'Rate limit exceeded',
302+
error_code: 429,
303+
errors: null,
304+
},
305+
},
306+
};
307+
const config = { retryLimit: 3 };
308+
const client = axios.create();
309+
310+
// Mock successful response after retry
311+
mock.onAny().reply(200, { success: true });
312+
313+
jest.useFakeTimers();
314+
315+
const responsePromise = retryResponseErrorHandler(error, config, client);
316+
317+
// Fast-forward time by 3 seconds (2 + 1 buffer)
318+
jest.advanceTimersByTime(3000);
319+
320+
const response: any = await responsePromise;
321+
322+
expect(response.status).toBe(200);
323+
expect(response.data.success).toBe(true);
324+
325+
jest.useRealTimers();
326+
});
327+
328+
it('should retry with default delay when x-ratelimit-remaining is 0 and no reset headers are present', async () => {
329+
const error = {
330+
config: { retryOnError: true, retryCount: 1 },
331+
response: {
332+
status: 429,
333+
headers: {
334+
'x-ratelimit-remaining': '0',
335+
},
336+
data: {
337+
error_message: 'Rate limit exceeded',
338+
error_code: 429,
339+
errors: null,
340+
},
341+
},
342+
};
343+
const config = { retryLimit: 3 };
344+
const client = axios.create();
345+
346+
// Mock successful response after retry
347+
mock.onAny().reply(200, { success: true });
348+
349+
// Use fake timers to avoid waiting for 60 seconds
350+
jest.useFakeTimers();
351+
352+
const responsePromise = retryResponseErrorHandler(error, config, client);
353+
354+
// Fast-forward time by 60 seconds
355+
jest.advanceTimersByTime(60000);
356+
357+
const response: any = await responsePromise;
358+
359+
expect(response.status).toBe(200);
360+
expect(response.data.success).toBe(true);
361+
362+
jest.useRealTimers();
363+
});
364+
365+
it('should reject with rate limit error when x-ratelimit-remaining is 0 and retry limit is exceeded', async () => {
366+
const error = {
367+
config: { retryOnError: true, retryCount: 3 }, // Already at retry limit
257368
response: {
258369
status: 429,
259370
headers: {

0 commit comments

Comments
 (0)