fix(deduplicate): settle the deduplicated request when it is aborted - #5673
fix(deduplicate): settle the deduplicated request when it is aborted#5673pacocartones wants to merge 1 commit into
Conversation
A request that joined an in-flight deduplicated request never settled if its signal aborted it: the synthetic waiting controller's abort() only flipped flags and marked the handler done, but nothing ever called the waiting handler's onResponseError, so the caller's promise hung forever. The same happened when the signal was already aborted at join time. The waiting controller's abort() now notifies the handler via onResponseError with the abort reason (RequestAbortedError when no reason was given), and errorWaitingHandler delegates the notification to abort() so each waiting 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 #5673 +/- ##
==========================================
- Coverage 93.43% 93.36% -0.08%
==========================================
Files 110 110
Lines 38733 38782 +49
==========================================
+ Hits 36190 36207 +17
- Misses 2543 2575 +32 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
For the reviewer: the 2 red jobs are the known flaky |
|
Heads-up on the red check: the only failure is |
This relates to...
No open issue found for this; reported directly with a reproduction.
Rationale
With
interceptors.deduplicate(), a request that joins an in-flight identicalrequest (the "waiting" request) never settles if it is aborted: its promise
neither resolves nor rejects, so callers hang forever (an
awaitnever returns,Promise.allnever completes). Two reachable variants, both reproduced againstcurrent
main(86b6299):still in flight. The abort reaches the waiting request's synthetic
controller (
abort(reason)inlib/handler/deduplication-handler.js), whichonly flips flags and marks the handler done — nothing ever calls the waiting
handler's
onResponseError, so the caller is never notified.onRequestStartaborts thesynthetic controller synchronously;
addWaitingHandlerseescontroller.aborted, marks the handler done and returnstrue(deduplicated"successfully") — again without ever erroring the request.
The primary request is unaffected in both cases and completes normally; only
the joined caller hangs.
Changes
abort(reason)now settles the waitingrequest: it notifies the handler via
onResponseErrorwith the abort reason(a
RequestAbortedErrorwhen no reason was given), guarded so a repeatedabort()notifies at most once. This covers both variants, since thealready-aborted case also flows through
controller.abort().#errorWaitingHandlernow delegates the notification tocontroller.abort(err)instead of callingonResponseErroritself, so eachwaiting handler is still errored exactly once.
abort: (reason) => { + if (state.aborted) { + return + } + state.aborted = true state.reason = reason ?? null waitingHandler.done = true waitingHandler.pendingTrailers = null waitingHandler.bufferedChunks = [] waitingHandler.bufferedBytes = 0 + + try { + handler.onResponseError?.(waitingHandler.controller, state.reason ?? new RequestAbortedError()) + } catch { + // Ignore errors from waiting handlers + } }Out of scope (deliberately not changed): aborting the primary request
propagates the error to all joined requests via
onResponseErrorinstead ofre-dispatching them independently. That pre-existing behavior is debatable but
works as designed and settles every caller; this PR only fixes the requests
that never settled.
Features
N/A
Bug Fixes
deduplicate(): a deduplicated (joined) request whose signal aborts nowrejects with the
AbortErrorinstead of hanging forever; same for a requestjoined with an already-aborted signal.
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 joined request never
settles, the 1000 ms race in the test wins)
(
actual: undefinedbecause the outcome is the literal'timed-out'from thePromise.racewatchdog: the request did not settle within 1000 ms while theprimary request to the same origin completed in ~100 ms.)
green — with the fix (must pass)
standalone reproduction against a live server (probe with explicit
settled-tracking, not just "no answer within Xs"): on main the joined request
is still unsettled 3000 ms after the primary resolved; with the fix it rejects
with
AbortError.(
V1= joined request aborted after joining;V2= joined with analready-aborted signal;
CTL= joined without abort, resolves normally.)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 deduplication handler and its
interceptor tests.
Status
Implementation and validation used AI assistance; I reviewed the final diff and results.