@@ -400,5 +400,78 @@ describe('HttpRetryPolicy', () => {
400400 expect ( operation . callCount ) . to . equal ( clientConfig . retryMaxAttempts ) ;
401401 }
402402 } ) ;
403+
404+ it ( 'should cancel a pending backoff sleep when the signal is aborted' , async ( ) => {
405+ const context = new ClientContextStub ( {
406+ retryDelayMin : 10000 ,
407+ retryDelayMax : 60000 ,
408+ retryMaxAttempts : 20 ,
409+ } ) ;
410+ const policy = new HttpRetryPolicy ( context ) ;
411+ const operation = sinon . stub ( ) . resolves ( {
412+ request : new Request ( 'http://localhost' ) ,
413+ response : new Response ( undefined , { status : 503 } ) ,
414+ } ) ;
415+ const controller = new AbortController ( ) ;
416+ const reason = new Error ( 'deadline' ) ;
417+ const clearTimeoutSpy = sinon . spy ( global , 'clearTimeout' ) ;
418+ const setTimeoutSpy = sinon . spy ( global , 'setTimeout' ) ;
419+
420+ try {
421+ const invocation = policy . invokeWithRetry ( operation , controller . signal ) ;
422+ // Let the first attempt complete and the 20s backoff sleep start
423+ await new Promise ( ( resolve ) => {
424+ setImmediate ( resolve ) ;
425+ } ) ;
426+ const backoffCall = setTimeoutSpy . getCalls ( ) . find ( ( call ) => call . args [ 1 ] === 20000 ) ;
427+ expect ( backoffCall ) . to . not . be . undefined ;
428+
429+ controller . abort ( reason ) ;
430+
431+ try {
432+ await invocation ;
433+ expect . fail ( 'It should reject with the abort reason' ) ;
434+ } catch ( error ) {
435+ if ( error instanceof AssertionError ) {
436+ throw error ;
437+ }
438+ expect ( error ) . to . equal ( reason ) ;
439+ }
440+ expect ( clearTimeoutSpy . calledWith ( backoffCall ! . returnValue ) ) . to . be . true ;
441+ expect ( operation . callCount ) . to . equal ( 1 ) ;
442+ } finally {
443+ setTimeoutSpy . restore ( ) ;
444+ clearTimeoutSpy . restore ( ) ;
445+ }
446+ } ) ;
447+
448+ it ( 'should not start another attempt when the signal is already aborted before the backoff' , async ( ) => {
449+ const context = new ClientContextStub ( {
450+ retryDelayMin : 10000 ,
451+ retryDelayMax : 60000 ,
452+ retryMaxAttempts : 20 ,
453+ } ) ;
454+ const policy = new HttpRetryPolicy ( context ) ;
455+ const controller = new AbortController ( ) ;
456+ const reason = new Error ( 'deadline' ) ;
457+ const operation = sinon . stub ( ) . callsFake ( async ( ) => {
458+ controller . abort ( reason ) ;
459+ return {
460+ request : new Request ( 'http://localhost' ) ,
461+ response : new Response ( undefined , { status : 503 } ) ,
462+ } ;
463+ } ) ;
464+
465+ try {
466+ await policy . invokeWithRetry ( operation , controller . signal ) ;
467+ expect . fail ( 'It should reject with the abort reason' ) ;
468+ } catch ( error ) {
469+ if ( error instanceof AssertionError ) {
470+ throw error ;
471+ }
472+ expect ( error ) . to . equal ( reason ) ;
473+ }
474+ expect ( operation . callCount ) . to . equal ( 1 ) ;
475+ } ) ;
403476 } ) ;
404477} ) ;
0 commit comments