Node.js — AbortController signal has no effect on a fetch
Fix uncancelled fetch calls by passing the signal correctly and distinguishing connect-time from body-read cancellation.
Symptoms
- Aborting the controller does not stop the request
- The promise resolves normally even after abort
- A timeout wraps fetch but the underlying socket stays open
- Listening code still receives data after abort
Possible causes
- The signal is created but never passed to fetch options
- The signal is passed to a wrapper that does not forward it
- The abort happens after the response object returned, during body consumption
- A custom HTTP client ignores the signal
Diagnosis steps
Confirm the signal reaches fetch
node -e "const c=new AbortController(); fetch(url,{signal:c.signal}).catch(e=>console.log(e.name)); c.abort()"Interpretation
The catch should log AbortError. If it does not, the signal was not forwarded to fetch.
Abort during body reading
node -e "const c=new AbortController(); fetch(url,{signal:c.signal}).then(r=>r.text()).catch(e=>console.log(e.name)); setTimeout(()=>c.abort(),10)"Interpretation
Aborting after headers arrive should reject the body read with AbortError if the signal is wired through the reader.
Check wrapper forwarding
grep -rn "signal" src/lib/httpInterpretation
A wrapper that builds options without including signal silently drops cancellation.
Root cause
The AbortSignal never reached the low-level request, or the abort was issued at a stage the client no longer treats as cancellable, so the request kept running despite the abort call.
Fix
Pass the signal into every fetch or client call, forward signals through wrappers and retry helpers, and abort during the correct phase, wiring timeouts to the same controller.
Verify the fix
Abort a request and confirm it rejects with AbortError quickly, and confirm the server or socket stops receiving data for that request.
Symptom walkthrough
Calling abort on an AbortController appears to do nothing: the fetch promise still resolves or hangs, and the request continues on the wire. The signal exists but is often not forwarded into the client options, or the abort fires after the response object is returned while the body is still being read, so the cancellation lands at a stage that is no longer observed.
Fix in practice
Thread the signal through every layer that constructs the request: direct calls, retry wrappers and helper functions. Tie timeouts to the same controller so one abort path covers both user cancellation and expiry, and be explicit about whether a request can be cancelled after headers arrive.
Verification
Abort a request and confirm it rejects promptly with an AbortError, then confirm the peer stops receiving data for that request. Both the client-side rejection and the server-side stop prove cancellation works.
Diagnostic commands
Check the behavior with commands like these:
node -e "const c=new AbortController(); fetch(url,{signal:c.signal}).catch(e=>console.log(e.name)); c.abort()"
node -e "const c=new AbortController(); fetch(url,{signal:c.signal}).then(r=>r.text()).catch(e=>console.log(e.name)); setTimeout(()=>c.abort(),10)"
grep -rn "signal" src/lib/http