Skip to content

fix: remove abort listener on successful fetch to prevent Deno hang#1823

Open
Yanhu007 wants to merge 1 commit intoopenai:masterfrom
Yanhu007:fix/fetch-timeout-abort-listener-leak
Open

fix: remove abort listener on successful fetch to prevent Deno hang#1823
Yanhu007 wants to merge 1 commit intoopenai:masterfrom
Yanhu007:fix/fetch-timeout-abort-listener-leak

Conversation

@Yanhu007
Copy link
Copy Markdown

Summary

Fixes #1811

When fetchWithTimeout receives an external signal, it adds an abort event listener to forward cancellation to its internal controller. On successful completion, the listener was never removed — only the internal timeout was cleared.

On Deno, AbortSignal.timeout() refs its underlying timer when listeners are attached. The orphaned listener kept the timer ref'd for the full timeout duration, preventing clean process exit even though the request completed in milliseconds.

Fix

Add signal.removeEventListener('abort', abort) in the existing finally block alongside clearTimeout(timeout).

Testing

  • Node.js: no behavior change (timer stays unref'd regardless of listeners)
  • Deno: process now exits immediately after successful request instead of hanging for the full signal timeout duration
  • Abort mid-request: still works correctly ({ once: true } fires before cleanup)

When fetchWithTimeout receives an external signal, it adds an abort
event listener to forward cancellation to its internal controller.
On successful completion, the listener was not removed.

On Deno, AbortSignal.timeout() refs its underlying timer when
listeners are attached. The orphaned listener kept the timer alive
for the full timeout duration, preventing clean process exit.

Add signal.removeEventListener in the finally block.

Fixes openai#1811
@Yanhu007 Yanhu007 requested a review from a team as a code owner April 12, 2026 00:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 575111d32f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/client.ts
// underlying timer, e.g. AbortSignal.timeout()) can be GC'd
// immediately. Without this, Deno keeps the timer ref'd for the
// full timeout duration, preventing clean process exit. See #1811.
if (signal) signal.removeEventListener('abort', abort);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve caller abort forwarding until body is consumed

Removing the caller-signal listener in fetchWithTimeout's finally detaches cancellation as soon as fetch() resolves (headers received), but this client parses/streams the body afterward via the same controller (see makeRequest returning response and defaultParseResponse consuming it later). In that window, aborting the user-provided signal no longer propagates, so long JSON reads or SSE streams can continue running even though the caller explicitly cancelled.

Useful? React with 👍 / 👎.

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.

fetchWithTimeout does not remove abort event listener on successful completion, preventing process exit on Deno

1 participant