Skip to content

fix: remove abort event listener from caller signal after request completes#1858

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

fix: remove abort event listener from caller signal after request completes#1858
xodn348 wants to merge 1 commit intoopenai:masterfrom
xodn348:fix/fetch-with-timeout-abort-listener-leak

Conversation

@xodn348
Copy link
Copy Markdown

@xodn348 xodn348 commented Apr 30, 2026

Summary

Fixes #1811.

When a caller passes a signal to fetchWithTimeout, an abort listener is added on that signal to forward abort events to the internal AbortController. With { once: true }, the listener cleans itself up when the signal fires — but if the request completes successfully (without the signal aborting), the listener is never removed.

On Deno, AbortSignal.timeout() refs its underlying timer whenever a listener is attached. The orphaned listener keeps the timer alive for the full timeout duration (e.g. 30 s), preventing the process from exiting even after the request returned in ~500 ms.

Fix: call signal.removeEventListener('abort', abort) in the finally block, alongside the existing clearTimeout(timeout). This mirrors the symmetric cleanup already done for the setTimeout.

   } finally {
     clearTimeout(timeout);
+    if (signal) signal.removeEventListener('abort', abort);
   }

The abort variable is already a named reference (created by this._makeAbort(controller)), so it can be passed to both addEventListener and removeEventListener without any other changes.

Test plan

  • Added a unit test (removes abort listener from caller signal after successful request) that spies on addEventListener/removeEventListener of the caller's signal and asserts:
    • the listener was added with { once: true }
    • removeEventListener was called with the same function reference after a successful fetch
  • All existing tests in tests/index.test.ts continue to pass (54/54)

…pletes

When a caller passes a `signal` to `fetchWithTimeout`, the method adds an
abort listener on that signal to forward abort events to the internal
`AbortController`. Previously the listener was only cleaned up when the
signal fired (via `{ once: true }`), but was never removed on a successful
or failed request completion.

On Deno, `AbortSignal.timeout()` refs its underlying timer whenever a
listener is attached. The orphaned listener kept the timer alive for the
full timeout duration, preventing the process from exiting even after the
request returned successfully.

Fix by calling `signal.removeEventListener('abort', abort)` in the
`finally` block, mirroring the existing `clearTimeout` cleanup.

Fixes openai#1811
@xodn348 xodn348 requested a review from a team as a code owner April 30, 2026 12:15
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