Skip to content

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

@jordanjennings

Description

@jordanjennings

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

  1. Create an OpenAI client
  2. Create an AbortSignal.timeout(30000)
  3. Pass it as the signal option to chat.completions.create()
  4. Request completes successfully in ~500ms
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions