Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## Change log
### Version: 1.2.4
#### Date: Aug-18-2025
- Fix: Retry request logic after rate limit replenishes

### Version: 1.2.3
#### Date: Aug-04-2025
- Fix: Added Pre-commit hook to run talisman and snyk scan
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/core",
"version": "1.2.3",
"version": "1.2.4",
"type": "commonjs",
"main": "./dist/cjs/src/index.js",
"types": "./dist/cjs/src/index.d.ts",
Expand Down
73 changes: 72 additions & 1 deletion src/lib/retryPolicy/delivery-sdk-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,30 @@ export const retryResponseErrorHandler = (error: any, config: any, axiosInstance
}
} else {
const rateLimitRemaining = response.headers['x-ratelimit-remaining'];

// Handle rate limit exhaustion with retry logic
if (rateLimitRemaining !== undefined && parseInt(rateLimitRemaining) <= 0) {
return Promise.reject(error.response.data);
retryCount++;

if (retryCount >= config.retryLimit) {
return Promise.reject(error.response.data);
}

error.config.retryCount = retryCount;

// Calculate delay for rate limit reset
const rateLimitResetDelay = calculateRateLimitDelay(response.headers);

return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const retryResponse = await axiosInstance(error.config);
resolve(retryResponse);
} catch (retryError) {
reject(retryError);
}
}, rateLimitResetDelay);
});
}

if (response.status == 429 || response.status == 401) {
Expand Down Expand Up @@ -99,3 +121,52 @@ const retry = (error: any, config: any, retryCount: number, retryDelay: number,
}, delayTime);
});
};

/**
* Calculate delay time for rate limit reset based on response headers
* @param headers - Response headers from the API
* @returns Delay time in milliseconds
*/
const calculateRateLimitDelay = (headers: any): number => {
// Check for retry-after header (in seconds)
const retryAfter = headers['retry-after'];
if (retryAfter) {
return parseInt(retryAfter) * 1000; // Convert to milliseconds
}

// Check for x-ratelimit-reset header (Unix timestamp)
const rateLimitReset = headers['x-ratelimit-reset'];
if (rateLimitReset) {
const resetTime = parseInt(rateLimitReset) * 1000; // Convert to milliseconds
const currentTime = Date.now();
const delay = resetTime - currentTime;

// Ensure we have a positive delay, add a small buffer
return Math.max(delay + 1000, 1000); // At least 1 second delay
}

// Check for x-ratelimit-reset-time header (ISO string)
const rateLimitResetTime = headers['x-ratelimit-reset-time'];
if (rateLimitResetTime) {
const resetTime = new Date(rateLimitResetTime).getTime();
const currentTime = Date.now();
const delay = resetTime - currentTime;

// Ensure we have a positive delay, add a small buffer
return Math.max(delay + 1000, 1000); // At least 1 second delay
}

// Default fallback delay (60 seconds) if no rate limit reset info is available
return 60000;
};

/**
* Retry request after specified delay
* @param error - The original error object
* @param delay - Delay time in milliseconds
* @param axiosInstance - Axios instance to retry with
* @returns Promise that resolves after the delay and retry
*/
const retryWithDelay = async (error: any, delay: number, axiosInstance: AxiosInstance) => {
return
};
113 changes: 112 additions & 1 deletion test/retryPolicy/delivery-sdk-handlers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,120 @@ describe('retryResponseErrorHandler', () => {
expect(retryCondition).toHaveBeenCalledWith(error);
});

it('should reject with rate limit error when x-ratelimit-remaining is 0', async () => {
it('should retry with delay when x-ratelimit-remaining is 0 and retry-after header is present', async () => {
const error = {
config: { retryOnError: true, retryCount: 1 },
response: {
status: 429,
headers: {
'x-ratelimit-remaining': '0',
'retry-after': '1', // 1 second for faster testing
},
data: {
error_message: 'Rate limit exceeded',
error_code: 429,
errors: null,
},
},
};
const config = { retryLimit: 3 };
const client = axios.create();

// Mock successful response after retry
mock.onAny().reply(200, { success: true });

jest.useFakeTimers();

const responsePromise = retryResponseErrorHandler(error, config, client);

// Fast-forward time by 1 second
jest.advanceTimersByTime(1000);

const response: any = await responsePromise;

expect(response.status).toBe(200);
expect(response.data.success).toBe(true);

jest.useRealTimers();
});

it('should retry with delay when x-ratelimit-remaining is 0 and x-ratelimit-reset header is present', async () => {
const error = {
config: { retryOnError: true, retryCount: 1 },
response: {
status: 429,
headers: {
'x-ratelimit-remaining': '0',
'x-ratelimit-reset': Math.floor((Date.now() + 2000) / 1000).toString(), // 2 seconds from now
},
data: {
error_message: 'Rate limit exceeded',
error_code: 429,
errors: null,
},
},
};
const config = { retryLimit: 3 };
const client = axios.create();

// Mock successful response after retry
mock.onAny().reply(200, { success: true });

jest.useFakeTimers();

const responsePromise = retryResponseErrorHandler(error, config, client);

// Fast-forward time by 3 seconds (2 + 1 buffer)
jest.advanceTimersByTime(3000);

const response: any = await responsePromise;

expect(response.status).toBe(200);
expect(response.data.success).toBe(true);

jest.useRealTimers();
});

it('should retry with default delay when x-ratelimit-remaining is 0 and no reset headers are present', async () => {
const error = {
config: { retryOnError: true, retryCount: 1 },
response: {
status: 429,
headers: {
'x-ratelimit-remaining': '0',
},
data: {
error_message: 'Rate limit exceeded',
error_code: 429,
errors: null,
},
},
};
const config = { retryLimit: 3 };
const client = axios.create();

// Mock successful response after retry
mock.onAny().reply(200, { success: true });

// Use fake timers to avoid waiting for 60 seconds
jest.useFakeTimers();

const responsePromise = retryResponseErrorHandler(error, config, client);

// Fast-forward time by 60 seconds
jest.advanceTimersByTime(60000);

const response: any = await responsePromise;

expect(response.status).toBe(200);
expect(response.data.success).toBe(true);

jest.useRealTimers();
});

it('should reject with rate limit error when x-ratelimit-remaining is 0 and retry limit is exceeded', async () => {
const error = {
config: { retryOnError: true, retryCount: 3 }, // Already at retry limit
response: {
status: 429,
headers: {
Expand Down
Loading