bun icon indicating copy to clipboard operation
bun copied to clipboard

AbortController usage crashes bun with uncatchable error

Open tobowers opened this issue 1 year ago • 2 comments

What version of Bun is running?

1.0.30 (but tested on 1.0.21 and 1.0.26 as well)

What platform is your computer?

darwin 23.4.0 arm (but crashing on our debian linux server too)

What steps can reproduce the bug?

Usage of the AbortController can crash bun in ways that are uncatchable with a AbortError: The operation was aborted. logged to the terminal. The process exits with a 1 status code .

Here's a very simple example:

import { setTimeout as sleep } from "node:timers/promises";

const abortController = new AbortController();

async function test() {
  await sleep(
    2000,
    () => {
      console.log("Timeout");
    },
    { signal: abortController.signal }
  );
}

test().catch((e) => {
  console.error("caught:", e);
});

abortController.abort();

We were seeing real-world usage using the OpenAI library and aborting while a response is streaming:

```typescript it("fails", async () => { const controller = new AbortController()
const client = new OpenAI({
  fetch: fetch
});

const resp = await client.chat.completions.create(
  {
    model: "gpt-4-0125-preview",
    messages: [{
      role: "system",
      content: "Answer in very long winded responses",
    },
    {
      role: "user",
      content: "What is the meaning of life?",
    }],
    stream: true,
  },
  {
    signal: controller.signal,
  }
)

setTimeout(() => {
  controller.abort()
}, 150)

for await (const res of resp) {
  try {
    console.log(".")
  } catch (err) {
    console.error('err: ', err)
  }
}

return new Promise((resolve) => setTimeout(resolve, 150))

})

</details>

Additional failure cases here with a simple SSE streaming server and a client that gets interrupted. https://github.com/dooart/temp-bun-abort-controller

### What is the expected behavior?

Expect these errors to be catchable.

### What do you see instead?

bun terminates the process with an exit code of 1

### Additional information

_No response_

tobowers avatar Mar 15 '24 08:03 tobowers

+1. As a workaround, using import fetch from 'node-fetch'; avoids this issue for plain HTTP requests. Still, this error is unrecoverable and we can't use the OpenAI SDK ourselves for streaming right now.

Edit: We've quelled our issue by adding the fetch key to the OpenAI client (more info here) and setting the value to a locally-imported node-fetch. This remains an issue with the global Bun implementation.

anthonykrivonos avatar Mar 18 '24 19:03 anthonykrivonos

are there any news about this issue?

Zig1375 avatar May 22 '24 10:05 Zig1375

Hello… is there any progress here? The issue still persists in version 1.1.20.

Zig1375 avatar Jul 26 '24 13:07 Zig1375

I can reproduce in 1.0.30 but not anymore in the last canary version (1.1.22) please reopen if is still happening after updating.

cirospaciari avatar Aug 07 '24 20:08 cirospaciari

I've spent the last two weeks trying to work out what is going on with the code here after upgrading to the latest Azure OpenAI SDK, and the issue is still occuring in Bun v1.1.26 with @azure/identity:4.5.0-beta2 and openai:4.56.1

Minimal test case here:

# index.ts
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity";
import { AzureOpenAI } from "openai";
import type { ChatCompletionMessageParam } from "openai/src/resources/index.js";

try {
	Bun.serve({
		async fetch() {
			try {
				const azureADTokenProvider = getBearerTokenProvider(
					new DefaultAzureCredential(),
					"https://cognitiveservices.azure.com/.default",
				);
				const endpoint = "<your-azure-deployment-url>";
				const deployment = "<your-azure-deploymentId>";
				const apiVersion = "2024-05-01-preview";
				const backend = new AzureOpenAI({ azureADTokenProvider, endpoint, apiVersion, deployment });
				const messages: ChatCompletionMessageParam[] = [
					{ role: "user", content: "Hello" },
				]
				
				const response = await backend?.chat.completions.create({ messages });
				
				return Response.json(response);
			} catch (error) {
				console.error("Local");
				console.error(error);
				return Response.json(error);
			}
		}
	});
} catch (error) {
	console.error("Global");
	console.error(error);
}

If you hit F5 a couple of times on the above code it'll crash the entire server with:

21 |  * }
22 |  * ```
23 |  */
24 | class AbortError extends Error {
25 |     constructor(message) {
26 |         super(message);
             ^
AbortError: The operation was aborted.
      at new AbortError (/.../<app>/app/node_modules/@azure/identity/node_modules/@azure/abort-controller/dist/commonjs/AbortError.js:26:9)
      at /.../<app>/app/node_modules/@azure/identity/node_modules/@azure/core-rest-pipeline/dist/commonjs/nodeHttpClient.js:282:25
      at abort (native:1:1)
      at /.../<app>/app/node_modules/@azure/identity/node_modules/@azure/core-rest-pipeline/dist/commonjs/nodeHttpClient.js:134:26

Neither local or global catch statements are hit in the above code, just a hard crash and zero output on the CLI leading you to think the server is still running but nothing is served any more.

Strangely it doesn't always fail immediately and acts more like a fast memory leak where it can take longer and longer to respond from the API until it fails.

I don't know if this issue is related to https://github.com/Azure/azure-sdk-for-js/issues/29112 or not but it seems impossible for me to fix "out of the box" although I will try the above "custom fetch" workaround.

randompixel avatar Aug 28 '24 14:08 randompixel

Alas no, the workaround isn't working for me. Tried passing fetch or node-fetch in to the AzureOpenAI class and both still crash on the second request with the above stack trace.

Looking at the stack trace it seems more related to the fetching of the tokens in the identity library rather than the actual calls to OpenAI. I have also tried passing an AbortController instance (and the old node package) to that but no difference

const azureADTokenProvider = getBearerTokenProvider(
    new DefaultAzureCredential(),
    "https://cognitiveservices.azure.com/.default",
    {
        abortSignal: new AbortController().signal,
    }
);

@cirospaciari can you re-open this or shall I create a new issue?

randompixel avatar Aug 28 '24 14:08 randompixel

I'm also having major issues with this while trying to cancel streaming with OpenAI. I've wrapped everything in try/catch, but it's truly uncatchable. This crashes Bun completely, and there are no remedies that seem to work. In my opinion, this needs high priority fix. I can't put aborting requests into production because when one user aborts their stream, it effectively aborts everyone else's too, as the process crashes.

Bun version v1.1.26 (macOS arm64).

240 |       try {
241 |         abortController.abort()
                              ^
AbortError: The operation was aborted.
 code: "ABORT_ERR"

mjp0 avatar Aug 31 '24 07:08 mjp0

@randompixel try adding an error callback in Bun.serve.

@mjp0 please open a new issue with a reproduction and we would be happy to help you. The error message there is not quite enough for us to help. Also, you should be able to use process.on("unhandledRejection", cb) or process.on("uncaughtException", cb)

Jarred-Sumner avatar Aug 31 '24 07:08 Jarred-Sumner

@randompixel try adding an error callback in Bun.serve.

@mjp0 please open a new issue with a reproduction and we would be happy to help you. The error message there is not quite enough for us to help. Also, you should be able to use process.on("unhandledRejection", cb) or process.on("uncaughtException", cb)

That worked, the process stays alive now! I'm just shaking my head that why didn't I think of listening the process directly 🤦🏼‍♂️

Sorry about not providing an example. I browsed a few of these issues and people had posted examples same as mine, so here's one from https://github.com/oven-sh/bun/issues/9805. It's basically your standard abortion.

const abortController = new AbortController();

async function test() {
  try {
    const response = await fetch('https://bun.sh/', {
      signal: abortController.signal
    });

    const stream = response.body;
    // @ts-ignore
    for await (const part of stream) {
      abortController.abort();
    }
  } catch (e: any) {
    if (e.name === 'AbortError') {
      console.log('Request was aborted');
    } else {
      console.error('An error occurred', e);
    }
  }

  console.log('Test completed');
}

test();

With the process monitoring, here's what comes out.

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
> Unhandled Rejection at: Promise { <rejected> } reason: 236 |   const abortSignal = abortController.signal

I don't know how to provide more helpful information but please do ask. I'd like to get this fixed as well in more robust way.

mjp0 avatar Aug 31 '24 07:08 mjp0

@mjp0 that particular example does not reproduce the issue.

Request was aborted
Test completed

This behaves consistent with Node.

Jarred-Sumner avatar Aug 31 '24 08:08 Jarred-Sumner

@mjp0 that particular example does not reproduce the issue.

Request was aborted
Test completed

This behaves consistent with Node.

Looks like Vercel's AI lib might have something extra special then.

import { createOpenAI } from "@ai-sdk/openai"
import { streamText } from "ai"

const abortController = new AbortController()
const abortSignal = abortController.signal

const opts = {
  apiKey: "key",
}

const m = createOpenAI(opts)

const call = await streamText({
  model: m.chat("gpt-4o"),
  messages: [
    {
      role: "user",
      content: "tell me a short story",
    },
  ],
  abortSignal: abortSignal,
})
for await (const textPart of call.textStream) {
  abortController.abort()
  console.log(textPart)
}

Gives me this:

AbortError: The operation was aborted.
 code: "ABORT_ERR"

      at /Users/marko/Dev/Temp/bun-abort/test.ts:25:21
20 |       }
21 |     ],
22 |     abortSignal: abortSignal,
23 |   })
24 |   for await (const textPart of call.textStream) {
25 |     abortController.abort()

mjp0 avatar Aug 31 '24 08:08 mjp0

@Jarred-Sumner I added the error callback exactly like the documentation and am still encountering the same issue. First time loads, silent crash on subsequent reloads.

Initially I was doing this in Elysia with a global error handler setup there before I made this minimal test case.

➜  app git:(bugfix/<branch>) ✗ bun run src/minimal.ts
21 |  * }
22 |  * ```
23 |  */
24 | class AbortError extends Error {
25 |     constructor(message) {
26 |         super(message);
             ^
AbortError: The operation was aborted.
      at new AbortError (<path>/cognitive-services-api/app/node_modules/@azure/identity/node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller/dist/commonjs/AbortError.js:26:9)
      at <path>/cognitive-services-api/app/node_modules/@azure/identity/node_modules/@azure/core-rest-pipeline/dist/commonjs/nodeHttpClient.js:282:25
      at abort (native:1:1)
      at <path>/cognitive-services-api/app/node_modules/@azure/identity/node_modules/@azure/core-rest-pipeline/dist/commonjs/nodeHttpClient.js:134:26

Bun v1.1.26 (macOS arm64)
➜  app git:(bugfix/<branch>) ✗

The commonality between my and @mjp0 's issue seems to be a dependency on the openai package, I wonder if there is something in the shims they use that is causing the issue.

randompixel avatar Aug 31 '24 10:08 randompixel

@Jarred-Sumner, why is this closed? This isn't fixed.

Even though catching unhandledRejections doesn't crash the Bun process, it still crashes the execution. In my case, I can't deploy aborting request feature because when Bun explodes with the unhandledRejection abort, the next step in the generation logic, which is logging the request (and abort), doesn't obviously execute anymore.

mjp0 avatar Sep 10 '24 06:09 mjp0

This is still an issue. Does anybody know a drop-in alternative for Bun that doesn't crash with basic stuff like this?

mjp0 avatar Oct 02 '24 13:10 mjp0

This is still an issue. Does anybody know a drop-in alternative for Bun that doesn't crash with basic stuff like this?

Deno 2 have quite good Node compatibility

dector avatar Oct 28 '24 16:10 dector