@@ -198,4 +198,148 @@ describe('retryAwareRequest', () => {
198198
199199 await expect ( networkRetryDisabled ) . rejects . toThrowError ( 'ENOTFOUND' )
200200 } )
201+
202+ test ( 'retries when request is aborted by client (AbortError message)' , async ( ) => {
203+ const mockRequestFn = vi
204+ . fn ( )
205+ . mockImplementationOnce ( ( ) => {
206+ throw new Error ( 'the operation was aborted' )
207+ } )
208+ . mockImplementationOnce ( ( ) => {
209+ return Promise . resolve ( {
210+ status : 200 ,
211+ data : { ok : true } ,
212+ headers : new Headers ( ) ,
213+ } )
214+ } )
215+
216+ const mockScheduleDelayFn = vi . fn ( ( fn , _delay ) => fn ( ) )
217+
218+ const result = retryAwareRequest (
219+ {
220+ request : mockRequestFn ,
221+ url : 'https://example.com/graphql.json' ,
222+ useNetworkLevelRetry : true ,
223+ maxRetryTimeMs : 2000 ,
224+ } ,
225+ undefined ,
226+ {
227+ defaultDelayMs : 10 ,
228+ scheduleDelay : mockScheduleDelayFn ,
229+ } ,
230+ )
231+
232+ await vi . runAllTimersAsync ( )
233+
234+ await expect ( result ) . resolves . toEqual ( {
235+ headers : expect . anything ( ) ,
236+ status : 200 ,
237+ data : { ok : true } ,
238+ } )
239+ expect ( mockRequestFn ) . toHaveBeenCalledTimes ( 2 )
240+ } )
241+
242+ test ( 'retries when fetch wrapper has blank reason in message' , async ( ) => {
243+ const blankReasonMessage = 'request to https://example.com/admin/api/unstable/graphql.json failed, reason:'
244+
245+ const mockRequestFn = vi
246+ . fn ( )
247+ . mockImplementationOnce ( ( ) => {
248+ throw new Error ( blankReasonMessage )
249+ } )
250+ . mockImplementationOnce ( ( ) => {
251+ return Promise . resolve ( {
252+ status : 200 ,
253+ data : { ok : true } ,
254+ headers : new Headers ( ) ,
255+ } )
256+ } )
257+
258+ const mockScheduleDelayFn = vi . fn ( ( fn , _delay ) => fn ( ) )
259+
260+ const result = retryAwareRequest (
261+ {
262+ request : mockRequestFn ,
263+ url : 'https://example.com/graphql.json' ,
264+ useNetworkLevelRetry : true ,
265+ maxRetryTimeMs : 2000 ,
266+ } ,
267+ undefined ,
268+ {
269+ defaultDelayMs : 10 ,
270+ scheduleDelay : mockScheduleDelayFn ,
271+ } ,
272+ )
273+
274+ await vi . runAllTimersAsync ( )
275+
276+ await expect ( result ) . resolves . toEqual ( {
277+ headers : expect . anything ( ) ,
278+ status : 200 ,
279+ data : { ok : true } ,
280+ } )
281+ expect ( mockRequestFn ) . toHaveBeenCalledTimes ( 2 )
282+ } )
283+
284+ test ( 'retries when blank reason contains trailing whitespace/newlines' , async ( ) => {
285+ const blankReasonWithWhitespace =
286+ 'request to https://example.com/admin/api/unstable/graphql.json failed, reason: \n\t'
287+
288+ const mockRequestFn = vi
289+ . fn ( )
290+ . mockImplementationOnce ( ( ) => {
291+ throw new Error ( blankReasonWithWhitespace )
292+ } )
293+ . mockImplementationOnce ( ( ) => {
294+ return Promise . resolve ( {
295+ status : 200 ,
296+ data : { ok : true } ,
297+ headers : new Headers ( ) ,
298+ } )
299+ } )
300+
301+ const result = retryAwareRequest (
302+ {
303+ request : mockRequestFn ,
304+ url : 'https://example.com/graphql.json' ,
305+ useNetworkLevelRetry : true ,
306+ maxRetryTimeMs : 2000 ,
307+ } ,
308+ undefined ,
309+ { defaultDelayMs : 10 , scheduleDelay : ( fn ) => fn ( ) } ,
310+ )
311+
312+ await vi . runAllTimersAsync ( )
313+
314+ await expect ( result ) . resolves . toEqual ( {
315+ headers : expect . anything ( ) ,
316+ status : 200 ,
317+ data : { ok : true } ,
318+ } )
319+ expect ( mockRequestFn ) . toHaveBeenCalledTimes ( 2 )
320+ } )
321+
322+ test ( 'does not treat non-blank reason as retryable when no known patterns match' , async ( ) => {
323+ vi . useRealTimers ( )
324+ const nonBlankUnknownReason =
325+ 'request to https://example.com/admin/api/unstable/graphql.json failed, reason: gateway policy'
326+
327+ const mockRequestFn = vi . fn ( ) . mockImplementationOnce ( ( ) => {
328+ throw new Error ( nonBlankUnknownReason )
329+ } )
330+
331+ const result = retryAwareRequest (
332+ {
333+ request : mockRequestFn ,
334+ url : 'https://example.com/graphql.json' ,
335+ useNetworkLevelRetry : true ,
336+ maxRetryTimeMs : 2000 ,
337+ } ,
338+ undefined ,
339+ { defaultDelayMs : 10 , scheduleDelay : ( fn ) => fn ( ) } ,
340+ )
341+
342+ await expect ( result ) . rejects . toThrowError ( nonBlankUnknownReason )
343+ expect ( mockRequestFn ) . toHaveBeenCalledTimes ( 1 )
344+ } )
201345} )
0 commit comments