-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fetchWithTimeout does not remove abort event listener on successful completion, preventing process exit on Deno #1811
Description
Confirm this is a Node library issue and not an underlying OpenAI API issue
- This is an issue with the Node library
Describe the bug
When running on Deno and setting timeouts via signals for LLM calls, the process does not cleanly exit until the full timeout has completed, even if the request returns successfully much sooner than the timeout.
I'm using OpenAI via LangChain, which passes an AbortSignal to the OpenAI SDK (e.g. via the signal option on chat.completions.create). The Deno process hangs for the full signal timeout duration after the request completes successfully. Without the signal option, the process exits immediately.
The root cause appears to be in fetchWithTimeout: the method adds an event listener on the caller's signal to forward abort events to its internal controller, but never removes the listener on successful completion. In v6.33.0 the listener uses { once: true } so it self-removes when the signal eventually aborts, but it is not removed when the request succeeds.
This matters on Deno because adding an event listener to AbortSignal.timeout() refs the underlying timer (unlike Node.js where it stays unref'd). The orphaned listener keeps the timer ref'd for the full timeout duration, preventing the process from exiting.
To Reproduce
- Create an OpenAI client
- Create an
AbortSignal.timeout(30000) - Pass it as the
signaloption tochat.completions.create() - Request completes successfully in ~500ms
- Process hangs for ~30 seconds instead of exiting
Without the signal option, the process exits immediately on both deno and node.
Verified that intercepting the listener and calling signal.removeEventListener("abort", capturedListener) after the call completes allows the process to exit immediately.
Code snippets
To reproduce the issue:
import OpenAI from "openai";
const client = new OpenAI();
const signal = AbortSignal.timeout(30000);
const response = await client.chat.completions.create(
{ model: "gpt-4o", messages: [{ role: "user", content: "Say hi" }], max_tokens: 10 },
{ signal },
);
console.log("Result:", response.choices[0].message.content);
// On Deno: hangs for ~30 seconds
// On Node.js: exits immediately (timer stays unref'd regardless of listeners)Potential fix — extract the anonymous function and removeEventListener in finally:
async fetchWithTimeout(url, init, ms, controller) {
const { signal, method, ...options } = init || {};
const listener = () => controller.abort();
if (signal)
signal.addEventListener('abort', listener);
const timeout = setTimeout(() => controller.abort(), ms);
// ...
try {
return await this.fetch.call(undefined, url, fetchOptions);
}
finally {
clearTimeout(timeout);
if (signal)
signal.removeEventListener('abort', listener);
}
}OS
macOS, Linux
Node version
Deno 2.7.1
Library version
openai v6.33.0