Skip to content

Commit 9fb9683

Browse files
Merge pull request #105 from contentstack/development
Development
2 parents 8cb5820 + 8b7f357 commit 9fb9683

File tree

4 files changed

+180
-71
lines changed

4 files changed

+180
-71
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
## Change log
2+
### Version: 1.2.2
3+
#### Date: Jun-09-2025
4+
- Enhancement: Retry logic to check for rate limit remaining header
5+
6+
### Version: 1.2.1
7+
#### Date: Apr-29-2025
8+
- Fix: Updated Regex for resolve the path traversal issue
9+
210
### Version: 1.2.0
311
#### Date: Jan-24-2025
412
- Fix: URL change for Live Preview

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.1",
3+
"version": "1.2.2",
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: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,26 @@ export const retryResponseErrorHandler = (error: any, config: any, axiosInstance
4343
} else {
4444
throw error;
4545
}
46-
} else if (response.status == 429 || response.status == 401) {
47-
retryCount++;
46+
} else {
47+
const rateLimitRemaining = response.headers['x-ratelimit-remaining'];
48+
if (rateLimitRemaining !== undefined && parseInt(rateLimitRemaining) <= 0) {
49+
return Promise.reject(error.response.data);
50+
}
51+
52+
if (response.status == 429 || response.status == 401) {
53+
retryCount++;
4854

49-
if (retryCount >= config.retryLimit) {
50-
if (error.response && error.response.data) {
51-
return Promise.reject(error.response.data);
55+
if (retryCount >= config.retryLimit) {
56+
if (error.response && error.response.data) {
57+
return Promise.reject(error.response.data);
58+
}
59+
60+
return Promise.reject(error);
5261
}
62+
error.config.retryCount = retryCount;
5363

54-
return Promise.reject(error);
64+
return axiosInstance(error.config);
5565
}
56-
error.config.retryCount = retryCount;
57-
58-
return axiosInstance(error.config);
5966
}
6067

6168
if (config.retryCondition && config.retryCondition(error)) {

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

Lines changed: 155 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,19 @@ describe('retryResponseErrorHandler', () => {
4646
await retryResponseErrorHandler(error, config, client);
4747
fail('Expected retryResponseErrorHandler to throw an error');
4848
} catch (err) {
49-
expect(err).toEqual(expect.objectContaining({
50-
code: 'ECONNABORTED',
51-
config: expect.objectContaining({ retryOnError: false }),
52-
}));
49+
expect(err).toEqual(
50+
expect.objectContaining({
51+
code: 'ECONNABORTED',
52+
config: expect.objectContaining({ retryOnError: false }),
53+
})
54+
);
5355
}
5456
});
5557
it('should reject the promise if retryOnError is true', async () => {
5658
const error = { config: { retryOnError: true } };
5759
const config = { retryLimit: 5 };
5860
const client = axios.create();
59-
61+
6062
try {
6163
await retryResponseErrorHandler(error, config, client);
6264
fail('Expected retryResponseErrorHandler to throw an error');
@@ -73,121 +75,212 @@ describe('retryResponseErrorHandler', () => {
7375
await retryResponseErrorHandler(error, config, client);
7476
fail('Expected retryResponseErrorHandler to throw an error');
7577
} catch (err) {
76-
expect(err).toEqual(expect.objectContaining({
77-
error_code: 408,
78-
error_message: `Timeout of ${config.timeout}ms exceeded`,
79-
errors: null
80-
}));
81-
}
78+
expect(err).toEqual(
79+
expect.objectContaining({
80+
error_code: 408,
81+
error_message: `Timeout of ${config.timeout}ms exceeded`,
82+
errors: null,
83+
})
84+
);
85+
}
8286
});
8387
it('should reject the promise if response status is 429 and retryCount exceeds retryLimit', async () => {
8488
const error = {
8589
config: { retryOnError: true, retryCount: 5 },
86-
response: { status: 429, statusText: 'timeout of 1000ms exceeded' },
90+
response: {
91+
status: 429,
92+
statusText: 'timeout of 1000ms exceeded',
93+
headers: {},
94+
data: {
95+
error_message: 'Rate limit exceeded',
96+
error_code: 429,
97+
errors: null,
98+
},
99+
},
87100
};
88101
const config = { retryLimit: 5, timeout: 1000 };
89102
const client = axios.create();
90103

91-
await expect(retryResponseErrorHandler(error, config, client)).rejects.toBe(error);
104+
await expect(retryResponseErrorHandler(error, config, client)).rejects.toEqual(error.response.data);
92105
});
93106
it('should reject the promise if response status is 401 and retryCount exceeds retryLimit', async () => {
94107
const error = {
95108
config: { retryOnError: true, retryCount: 5 },
96-
response: { status: 401, statusText: 'timeout of 1000ms exceeded' },
109+
response: {
110+
status: 401,
111+
statusText: 'timeout of 1000ms exceeded',
112+
headers: {},
113+
data: {
114+
error_message: 'Unauthorized',
115+
error_code: 401,
116+
errors: null,
117+
},
118+
},
97119
};
98120
const config = { retryLimit: 5, timeout: 1000 };
99121
const client = axios.create();
100122

101-
await expect(retryResponseErrorHandler(error, config, client)).rejects.toBe(error);
123+
await expect(retryResponseErrorHandler(error, config, client)).rejects.toEqual(error.response.data);
102124
});
103125
it('should reject the promise if response status is 429 or 401 and retryCount is within limit', async () => {
104126
const error = {
105127
config: { retryOnError: true, retryCount: 4 },
106-
response: { status: 429, statusText: 'timeout of 1000ms exceeded' },
128+
response: {
129+
status: 429,
130+
statusText: 'timeout of 1000ms exceeded',
131+
headers: {},
132+
data: {
133+
error_message: 'Rate limit exceeded',
134+
error_code: 429,
135+
errors: null,
136+
},
137+
},
107138
request: {
108139
method: 'post',
109140
url: '/retryURL',
110141
data: { key: 'value' },
111142
headers: { 'Content-Type': 'application/json' },
112143
},
113144
};
114-
const config = { retryLimit: 5, timeout: 1000 };
145+
const config = { retryLimit: 4, timeout: 1000 };
115146
const client = axios.create();
116147

117-
const finalResponseObj = {
118-
config: { retryOnError: true, retryCount: 4 },
119-
response: { status: 429, statusText: 'timeout of 1000ms exceeded' },
120-
};
121-
122-
mock.onPost('/retryURL').reply(200, finalResponseObj);
123-
124-
try {
125-
await retryResponseErrorHandler(error, config, client);
126-
throw new Error('Expected retryResponseErrorHandler to throw an error');
127-
} catch (err: any) {
128-
expect(err.response.status).toBe(429);
129-
expect(err.response.statusText).toBe(error.response.statusText);
130-
expect(err.config.retryCount).toBe(error.config.retryCount);
131-
}
132-
148+
await expect(retryResponseErrorHandler(error, config, client)).rejects.toEqual(error.response.data);
133149
});
134150
it('should call the retry function if retryCondition is passed', async () => {
135151
const error = {
136152
config: { retryOnError: true, retryCount: 4 },
137-
response: { status: 200, statusText: 'Success Response but retry needed' },
153+
response: {
154+
status: 200,
155+
statusText: 'Success Response but retry needed',
156+
headers: {},
157+
data: {
158+
error_message: 'Retry needed',
159+
error_code: 200,
160+
errors: null,
161+
},
162+
},
138163
request: {
139164
method: 'post',
140165
url: '/retryURL',
141166
data: { key: 'value' },
142167
headers: { 'Content-Type': 'application/json' },
143168
},
144169
};
145-
// eslint-disable-next-line @typescript-eslint/no-shadow
146-
const retryCondition = (error: any) => true;
147-
const config = { retryLimit: 5, timeout: 1000, retryCondition: retryCondition };
170+
const retryCondition = () => true;
171+
const config = { retryLimit: 5, timeout: 1000, retryCondition };
148172
const client = axios.create();
149173

150-
const finalResponseObj = {
151-
config: { retryOnError: true, retryCount: 5 },
152-
response: { status: 429, statusText: 'timeout of 1000ms exceeded' },
153-
};
154-
155-
mock.onPost('/retryURL').reply(200, finalResponseObj);
174+
mock.onPost('/retryURL').reply(200, { success: true });
156175

157-
const finalResponse: any = await retryResponseErrorHandler(error, config, client);
158-
159-
expect(finalResponse.data).toEqual(finalResponseObj);
176+
const response = (await retryResponseErrorHandler(error, config, client)) as AxiosResponse;
177+
expect(response.status).toBe(200);
160178
});
161179
it('should reject to error when retryCondition is passed but retryLimit is exceeded', async () => {
162180
const error = {
163181
config: { retryOnError: true, retryCount: 5 },
164-
response: { status: 200, statusText: 'Success Response but retry needed' },
182+
response: {
183+
status: 200,
184+
statusText: 'Success Response but retry needed',
185+
headers: {},
186+
data: {
187+
error_message: 'Retry needed',
188+
error_code: 200,
189+
errors: null,
190+
},
191+
},
165192
request: {
166193
method: 'post',
167194
url: '/retryURL',
168195
data: { key: 'value' },
169196
headers: { 'Content-Type': 'application/json' },
170197
},
171198
};
172-
// eslint-disable-next-line @typescript-eslint/no-shadow
173199
const retryCondition = (error: any) => true;
174-
const config = { retryLimit: 5, timeout: 1000, retryCondition: retryCondition };
200+
const config = { retryLimit: 5, timeout: 1000, retryCondition };
175201
const client = axios.create();
176202

177-
const finalResponseObj = {
178-
config: { retryOnError: true, retryCount: 5 },
179-
response: { status: 429, statusText: 'timeout of 1000ms exceeded' },
203+
await expect(retryResponseErrorHandler(error, config, client)).rejects.toEqual(error);
204+
});
205+
206+
it('should retry when response status is 429 and retryCount is less than retryLimit', async () => {
207+
const error = {
208+
config: { retryOnError: true, retryCount: 1 },
209+
response: {
210+
status: 429,
211+
statusText: 'Rate limit exceeded',
212+
headers: {},
213+
data: {
214+
error_message: 'Rate limit exceeded',
215+
error_code: 429,
216+
errors: null,
217+
},
218+
},
219+
};
220+
const config = { retryLimit: 3 };
221+
const client = axios.create();
222+
223+
mock.onAny().reply(200, { success: true });
224+
225+
const response = (await retryResponseErrorHandler(error, config, client)) as AxiosResponse;
226+
expect(response.status).toBe(200);
227+
});
228+
229+
it('should retry when retryCondition is true', async () => {
230+
const error = {
231+
config: { retryOnError: true, retryCount: 1 },
232+
response: {
233+
status: 500,
234+
statusText: 'Internal Server Error',
235+
headers: {},
236+
data: {
237+
error_message: 'Internal Server Error',
238+
error_code: 500,
239+
errors: null,
240+
},
241+
},
180242
};
243+
const retryCondition = jest.fn().mockReturnValue(true);
244+
const config = { retryLimit: 3, retryCondition, retryDelay: 100 };
245+
const client = axios.create();
181246

182-
mock.onPost('/retryURL').reply(200, finalResponseObj);
247+
mock.onAny().reply(200, { success: true });
183248

184-
await expect(retryResponseErrorHandler(error, config, client)).rejects.toBe(error);
249+
const response = (await retryResponseErrorHandler(error, config, client)) as AxiosResponse;
250+
expect(response.status).toBe(200);
251+
expect(retryCondition).toHaveBeenCalledWith(error);
185252
});
186253

187-
it('should retry when response status is 429 and retryCount is less than retryLimit', async () => {
254+
it('should reject with rate limit error when x-ratelimit-remaining is 0', async () => {
188255
const error = {
189256
config: { retryOnError: true, retryCount: 1 },
190-
response: { status: 429, statusText: 'Rate limit exceeded' },
257+
response: {
258+
status: 429,
259+
headers: {
260+
'x-ratelimit-remaining': '0',
261+
},
262+
data: {
263+
error_message: 'Rate limit exceeded',
264+
error_code: 429,
265+
errors: null,
266+
},
267+
},
268+
};
269+
const config = { retryLimit: 3 };
270+
const client = axios.create();
271+
272+
await expect(retryResponseErrorHandler(error, config, client)).rejects.toEqual(error.response.data);
273+
});
274+
275+
it('should retry when x-ratelimit-remaining is greater than 0', async () => {
276+
const error = {
277+
config: { retryOnError: true, retryCount: 1 },
278+
response: {
279+
status: 429,
280+
headers: {
281+
'x-ratelimit-remaining': '5',
282+
},
283+
},
191284
};
192285
const config = { retryLimit: 3 };
193286
const client = axios.create();
@@ -198,19 +291,20 @@ describe('retryResponseErrorHandler', () => {
198291
expect(response.status).toBe(200);
199292
});
200293

201-
it('should retry when retryCondition is true', async () => {
294+
it('should retry when x-ratelimit-remaining header is not present', async () => {
202295
const error = {
203296
config: { retryOnError: true, retryCount: 1 },
204-
response: { status: 500, statusText: 'Internal Server Error' },
297+
response: {
298+
status: 429,
299+
headers: {},
300+
},
205301
};
206-
const retryCondition = jest.fn().mockReturnValue(true);
207-
const config = { retryLimit: 3, retryCondition, retryDelay: 100 };
302+
const config = { retryLimit: 3 };
208303
const client = axios.create();
209304

210305
mock.onAny().reply(200);
211306

212307
const response: any = await retryResponseErrorHandler(error, config, client);
213308
expect(response.status).toBe(200);
214-
expect(retryCondition).toHaveBeenCalledWith(error);
215309
});
216310
});

0 commit comments

Comments
 (0)