|
8 | 8 | getRetryDelay, |
9 | 9 | } from '../../src/lib/retryPolicy/delivery-sdk-handlers'; |
10 | 10 | import MockAdapter from 'axios-mock-adapter'; |
| 11 | +import { APIError } from '../../src/lib/api-error'; |
11 | 12 |
|
12 | 13 | describe('retryRequestHandler', () => { |
13 | 14 | it('should add retryCount to the request config', () => { |
@@ -120,6 +121,52 @@ describe('retryResponseErrorHandler', () => { |
120 | 121 | jest.useRealTimers(); |
121 | 122 | }); |
122 | 123 |
|
| 124 | + it.each(['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'EPIPE', 'EAI_AGAIN'])( |
| 125 | + 'should retry transient network error %s by default when no custom retryCondition is configured', |
| 126 | + async (code) => { |
| 127 | + const error = { |
| 128 | + config: { retryOnError: true, retryCount: 1, method: 'get', url: '/default-retry' }, |
| 129 | + code, |
| 130 | + message: `simulated ${code}`, |
| 131 | + }; |
| 132 | + // No retryCondition here - mirrors the raw StackConfig a real stack() caller passes. |
| 133 | + const config = { retryLimit: 3, retryDelay: 50 }; |
| 134 | + const client = axios.create(); |
| 135 | + |
| 136 | + mock.onGet('/default-retry').reply(200, { success: true }); |
| 137 | + |
| 138 | + jest.useFakeTimers(); |
| 139 | + |
| 140 | + const responsePromise = retryResponseErrorHandler(error, config, client); |
| 141 | + jest.advanceTimersByTime(50); |
| 142 | + |
| 143 | + const response = (await responsePromise) as AxiosResponse; |
| 144 | + expect(response.status).toBe(200); |
| 145 | + |
| 146 | + jest.useRealTimers(); |
| 147 | + } |
| 148 | + ); |
| 149 | + |
| 150 | + it.each(['ENOTFOUND', 'ECONNREFUSED'])( |
| 151 | + 'should not retry non-transient network error %s by default when no custom retryCondition is configured', |
| 152 | + async (code) => { |
| 153 | + const error = { |
| 154 | + config: { retryOnError: true, retryCount: 1 }, |
| 155 | + code, |
| 156 | + message: `simulated ${code}`, |
| 157 | + }; |
| 158 | + const config = { retryLimit: 3 }; |
| 159 | + const client = axios.create(); |
| 160 | + |
| 161 | + try { |
| 162 | + await retryResponseErrorHandler(error, config, client); |
| 163 | + fail(`Expected retryResponseErrorHandler to throw for ${code}`); |
| 164 | + } catch (err) { |
| 165 | + expect(err).toEqual(error); |
| 166 | + } |
| 167 | + } |
| 168 | + ); |
| 169 | + |
123 | 170 | it('should rethrow network errors when retryCondition returns false', async () => { |
124 | 171 | const error = { |
125 | 172 | config: { retryOnError: true, retryCount: 1 }, |
@@ -201,23 +248,43 @@ describe('retryResponseErrorHandler', () => { |
201 | 248 | jest.useRealTimers(); |
202 | 249 | }); |
203 | 250 |
|
204 | | - it('should resolve the promise to 408 error if retryOnError is true and error code is ECONNABORTED', async () => { |
| 251 | + it('should throw a real Error with the timeout duration and a TIMEOUT code when ECONNABORTED occurs', async () => { |
205 | 252 | const error = { config: { retryOnError: true, retryCount: 1 }, code: 'ECONNABORTED' }; |
206 | | - const config = { retryLimit: 5, timeout: 1000 }; |
| 253 | + // retryCondition explicitly disabled here to isolate the non-retried ECONNABORTED throw path, |
| 254 | + // since ECONNABORTED is retried by default now (see "should retry transient network error" tests). |
| 255 | + const config = { retryLimit: 5, timeout: 1000, retryCondition: () => false }; |
207 | 256 | const client = axios.create(); |
208 | 257 | try { |
209 | 258 | await retryResponseErrorHandler(error, config, client); |
210 | 259 | fail('Expected retryResponseErrorHandler to throw an error'); |
211 | | - } catch (err) { |
212 | | - expect(err).toEqual( |
213 | | - expect.objectContaining({ |
214 | | - error_code: 408, |
215 | | - error_message: `Request timeout of ${config.timeout}ms exceeded. Please try again or increase the timeout value in your configuration.`, |
216 | | - errors: null, |
217 | | - }) |
| 260 | + } catch (err: any) { |
| 261 | + expect(err).toBeInstanceOf(Error); |
| 262 | + expect(err.message).toBe( |
| 263 | + `Request timeout of ${config.timeout}ms exceeded. Please try again or increase the timeout value in your configuration.` |
218 | 264 | ); |
| 265 | + expect(err.code).toBe(408); |
219 | 266 | } |
220 | 267 | }); |
| 268 | + it('should classify a request timeout distinctly instead of as an unknown error', async () => { |
| 269 | + const error = { config: { retryOnError: true, retryCount: 1 }, code: 'ECONNABORTED' }; |
| 270 | + // retryCondition explicitly disabled to isolate the non-retried classification path - |
| 271 | + // ECONNABORTED is retried by default now (see "should retry transient network error" tests). |
| 272 | + const config = { retryLimit: 5, timeout: 1000, retryCondition: () => false }; |
| 273 | + const client = axios.create(); |
| 274 | + |
| 275 | + let thrown: any; |
| 276 | + try { |
| 277 | + await retryResponseErrorHandler(error, config, client); |
| 278 | + fail('Expected retryResponseErrorHandler to throw an error'); |
| 279 | + } catch (err) { |
| 280 | + thrown = err; |
| 281 | + } |
| 282 | + |
| 283 | + const apiError = APIError.fromAxiosError(thrown); |
| 284 | + |
| 285 | + expect(apiError.error_code).toBe(408); |
| 286 | + expect(apiError.error_message).toContain('timeout'); |
| 287 | + }); |
221 | 288 | it('should reject the promise if response status is 429 and retryCount exceeds retryLimit', async () => { |
222 | 289 | const error = { |
223 | 290 | config: { retryOnError: true, retryCount: 5 }, |
|
0 commit comments