fix(retry): honor the abort signal during the retry backoff - #5675
Merged
mcollina merged 3 commits intoAug 14, 2026
Conversation
An abort that arrived while the retry policy was holding a backoff timer was ignored until the timer elapsed: the default policy created an untracked setTimeout(), and the consumer's abort only reached the controller of the already-finished connection, a no-op. The request then rejected with the AbortError only after the full backoff (5s in the repro, minutes with a large Retry-After), and the referenced timer kept the process alive meanwhile. The default policy now returns its backoff timer and the handler keeps it while a retry decision is pending. The controller proxy reports abort() to the handler, which cancels the pending wait and forwards the abort reason via onResponseError immediately. Late policy callbacks and the torn-down connection's error are guarded so the downstream handler is errored exactly once. Signed-off-by: pacocartones <manusanchezhl@gmail.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5675 +/- ##
==========================================
- Coverage 93.46% 93.44% -0.02%
==========================================
Files 110 110
Lines 38777 38840 +63
==========================================
+ Hits 36241 36293 +52
- Misses 2536 2547 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5 tasks
Contributor
Author
|
Heads-up on the red check: the only failure is |
metcoder95
reviewed
Aug 13, 2026
Review feedback: `onAbort` was a public method of `RetryHandler`, so anything holding the handler could cancel a pending retry and error the downstream handler. It is now a private method, and `RetryController` no longer holds a reference to the handler at all: it receives the notification as a private callback at construction. The only way to reach it is the proxy's own `abort()` -- the path a consumer abort already takes. No behaviour change: same tests, same call path. Signed-off-by: pacocartones <manusanchezhl@gmail.com>
7 tasks
metcoder95
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This relates to...
No open issue found for this; reported directly with a reproduction.
Rationale
When a retry is scheduled, the default retry policy parks the decision in a
setTimeoutbackoff (lib/handler/retry-handler.js). An abort that arriveswhile that backoff is pending is ignored until the timer elapses. The
consumer's abort reaches the controller of the connection that already
finished — a no-op — and the pending timer is not tracked anywhere, so the
request only rejects with the
AbortErrorafter the full backoff: 5 s in therepro below, and minutes with a large
Retry-After(e.g.Retry-After: 120holds the caller for two minutes). The referenced (non-
unref) timer alsokeeps the process alive for the whole backoff even though nothing will come of
it.
Reproduced against current
main(86b6299): a server that always answers 500,a
RetryAgentwithminTimeout: 5000, andabort()fired 800 ms into therequest. The rejection arrives at ~5038 ms instead of ~800 ms.
Changes
tracks it while a retry decision is pending (
retryPending/retryTimer).RetryControllerproxy — where every consumer abort already funnelsthrough — now also reports
abort(reason)to the handler.onAbortcancelsthe pending wait and forwards the abort reason downstream via
onResponseErrorimmediately (aRequestAbortedErrorwhen no reason wasgiven, matching
api-request's convention).(e.g. a custom retry function resolving after the abort) is ignored, and the
torn-down connection's late error is not forwarded twice, so the downstream
handler is errored exactly once.
Both policy call sites (the
throwOnError: falseresponse path and theonResponseErrorpath used bythrowOnError: trueand network errors) trackthe pending decision the same way.
Scope note: a custom retry function that keeps its own timer and does not
return it cannot have that timer cancelled (we were never handed it), but the
abort still settles the request immediately and the late callback is ignored.
Out of scope (deliberately not changed):
unref()-ing the backoff timer. Apending retry is a live logical request; letting the process exit mid-backoff
would silently abandon it. That idea is being explored in #4470, which is
orthogonal to this fix.
Features
N/A
Bug Fixes
retry()/RetryAgent: an abort that arrives while a retry backoff ispending now rejects immediately with the
AbortErrorinstead of after thefull backoff, and no retry is dispatched after the abort.
Breaking Changes and Deprecations
None.
Verification
Every command below was run and its output captured verbatim. The record is
reproducible — the exact commands are included so you can re-run them yourself.
red — main (86b6299) without the fix (must fail: the rejection only comes
after the full backoff)
(The abort is fired while the backoff is pending — the test hooks the retry
policy invocation — yet the request only settles once the 10 s / 3 s timers
elapse.)
green — with the fix (must pass)
(The second test's ~3.5 s are deliberate: after the immediate rejection it
waits past the custom policy's 3 s timer to assert the late decision is
ignored and no retry is dispatched. The assertions are relational — abort
during backoff → immediate rejection with the abort reason — with margins of
two orders of magnitude, so CI slowness does not flake them.)
standalone reproduction against a live server (
RetryAgentwithminTimeout: 5000, server always 500,abort()at 800 ms):area suites with the fix (must pass)
lint (must pass)
Not verified locally: the remaining test suites (
npm testruns 14 of them);left to CI, as this change only touches the retry handler and its tests. No
documentation changes: no existing doc describes abort-during-backoff
behavior, and the fixed behavior now matches the general abort contract;
happy to add a note to
docs/docs/api/RetryHandler.mdif maintainers want one.Status
Implementation and validation used AI assistance; I reviewed the final diff and results.