Confirm this is a Node library issue and not an underlying OpenAI API issue
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
signal option to chat.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
Confirm this is a Node library issue and not an underlying OpenAI API issue
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
AbortSignalto the OpenAI SDK (e.g. via thesignaloption onchat.completions.create). The Deno process hangs for the full signal timeout duration after the request completes successfully. Without thesignaloption, 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
AbortSignal.timeout(30000)signaloption tochat.completions.create()Without the
signaloption, 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:
Potential fix — extract the anonymous function and
removeEventListenerinfinally:OS
macOS, Linux
Node version
Deno 2.7.1
Library version
openai v6.33.0