Skip to content

Conversation

mcollina
Copy link
Member

Summary

Fixes issue where aborting fetch requests to unresponsive hosts would cause the Node.js process to hang for 8+ seconds instead of exiting cleanly.

Problem

When calling fetch() with an unresponsive host and then aborting the request early, the process would hang waiting for internal connection timeouts to expire. This was caused by connection timeout timers in setupConnectTimeout that were not being cancelled when the fetch was aborted.

Root Cause

  1. fetch() creates a connection to an unresponsive host
  2. setupConnectTimeout in lib/core/util.js creates internal timers
  3. When fetch() is aborted, the connection attempt continues in background
  4. Internal timers keep the event loop alive for ~10 seconds
  5. Process cannot exit cleanly

Solution

  • Enhanced lib/core/connect.js: Expose clearConnectTimeout function on socket
  • Enhanced lib/dispatcher/client.js: Cancel connection timeout when client is destroyed during connection
  • Added test: Comprehensive test case in test/fetch/issue-4405.js

Verification

# Before fix: Process hangs for 8+ seconds
node test/fetch/issue-4405.js
# After fix: Process exits cleanly in ~1 second ✅

Test Results

  • ✅ New test passes (process exits in ~1.1s instead of hanging)
  • ✅ Existing tests continue to pass
  • ✅ No breaking changes to public API

Fixes #4405

🤖 Generated with Claude Code

mcollina and others added 8 commits August 12, 2025 17:05
This test reproduces a bug where aborting a fetch request to an
unresponsive host prevents the Node.js process from exiting cleanly.
The process hangs for ~8-10 seconds due to internal timers/handles
remaining referenced in the event loop after the abort signal.

The test uses a worker thread to isolate the issue and demonstrates
that while the fetch operation completes and is aborted within ~1s,
the process takes significantly longer to exit.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Matteo Collina <[email protected]>
…4405

This commit addresses the process hanging issue when aborting requests
to unresponsive hosts. Instead of monkey-patching the destroy method,
we now properly handle the close event to ensure timers and handles
are cleaned up when connections are closed or aborted.

This prevents internal timers from keeping the event loop alive
after a request has been aborted, allowing the Node.js process
to exit cleanly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Matteo Collina <[email protected]>
Remove monkey-patching of socket.destroy and use the canonical 'close'
event instead for cleanup. This follows Node.js core patterns and is
more maintainable.

The close event is the definitive cleanup event for all Node.js streams
and sockets, covering all closure scenarios (destroy, timeout, error,
abort, etc.) without modifying existing methods.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Matteo Collina <[email protected]>
Restore util.js from main branch and remove unused kClearConnectTimeout
symbol since the improved implementation using close event listeners
doesn't require these changes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Matteo Collina <[email protected]>
The close event listener approach doesn't actually solve the process hanging issue.
The problem likely lies in fetch's FinalizationRegistry logic, not in the connect timeout handling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Enhanced abort handling in multiple areas:
- Added removeAllListeners() to Fetch.abort()
- Improved connection.destroy() to clean up references
- Added dispatch-level abort handling for terminated controller
- Added cleanup of abort listener in main fetch function

However, the process still hangs for ~8 seconds when aborting requests to
unresponsive hosts. The fetch itself aborts correctly (TypeError), but
underlying timers/handles remain active, preventing clean process exit.

The issue appears to be in the Agent/Client layer where internal connection
timeouts are not properly cancelled on abort.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Added mechanism to track and cleanup sockets during connection when Client
is destroyed:
- Added kConnectingAbort symbol to track abort function
- Store socket reference in connector callback for cleanup
- Call socket.destroy() when Client is destroyed during connection

However, the issue persists because for unresponsive hosts:
- The connector callback is never called (connection hangs at TCP level)
- The socket is created but setupConnectTimeout manages the cleanup
- Internal timers from setupConnectTimeout are not properly cancelled on abort

Root cause: setupConnectTimeout in lib/core/util.js creates timers that are
not cancelled when fetch is aborted early, keeping event loop alive.

Need to ensure connection timeouts are cancelled when fetch controller aborts.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit fixes a process hanging issue when aborting fetch requests
to unresponsive hosts. Previously, the connection timeout mechanism
(setupConnectTimeout) created timers that were not cancelled when a
fetch was aborted early, keeping the Node.js event loop alive.

Changes:
- Expose clearConnectTimeout function on socket in lib/core/connect.js
- Cancel connection timeout in Client abort logic in lib/dispatcher/client.js
- Add comprehensive test for fetch abort to unresponsive host scenarios

The fix ensures that when a fetch is aborted while connecting to an
unresponsive host, all internal timers are properly cleaned up,
allowing the Node.js process to exit cleanly instead of hanging.

Fixes #4405

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Matteo Collina <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Process does not exit after aborting a request to an unresponsive host
1 participant