bun icon indicating copy to clipboard operation
bun copied to clipboard

fetch signal abortcontroller does not work

Open sroussey opened this issue 2 years ago • 21 comments

What version of Bun is running?

0.5.8

What platform is your computer?

Darwin 22.3.0 arm64 arm

What steps can reproduce the bug?

interface FetchWithTimeoutOptions extends Omit<FetchRequestInit, 'timeout'> {
  timeout: number;
}
export async function fetchWithTimeout(resource: string | URL, options?: FetchWithTimeoutOptions) {
  const { timeout = 1000, ...otheroptions } = options || {};

  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  let response;
  try {
    response = await fetch(resource, {
      ...otheroptions,
      signal: controller.signal,
    });
  } catch (err) {
    response = {
      ok: false,
      status: 500,
    };
  }
  clearTimeout(id);
  return response;
}

const start = Date.now();
await fetchWithTimeout("https://www.custvestor.com/", { timeout: 100 });
const end = Date.now();
console.log(`It took ${end-start}ms not 100ms`);

What is the expected behavior?

It should take 100ms

What do you see instead?

It takes a lot longer!

Additional information

No response

sroussey avatar Mar 26 '23 19:03 sroussey

@Plecra This seems to also apply to plain HTTP connections. If the connection cannot be established (all packets dropped at the destination), signals do nothing. Only the internal HTTP timeout is applied (1 minute?).

I tested AbortSignal and AbortConroller on fetch from http://127.0.0.7, while nothing was listening there. Both tests result with Signal failed! on macOS Darwin 21.6.0 x86_64 with Bun 1.0.14

const timeout = setTimeout(() => {
	console.error('Signal failed!');
	process.exit();
}, 2e3);

try {
	console.log(await fetch('http://127.0.0.7', { signal: AbortSignal.timeout(1e3) }));
}
catch (e) {
	clearTimeout(timeout);
	if (e.name === 'TimeoutError') console.log('Signal worked!');
	else console.error(e);
}

I'm also pinging @cirospaciari, since he authored https://github.com/oven-sh/bun/pull/6390

uxmaster avatar Nov 29 '23 10:11 uxmaster

I am facing the same issue on AbortSignal.timeout as @uxmaster on the latest bun canary build (1.0.20+05984d405) as well.

See also #7512 and #7513.

SukkaW avatar Dec 26 '23 06:12 SukkaW

Weirdly if i let this run I get Aborted! as the output but it takes the same amount of time to run.

➜  stable-diffusion-discord-bot git:(main) ✗ bun run src/file.ts
[75.02s] fetch
Aborted!
➜  stable-diffusion-discord-bot git:(main) ✗ bun run src/file.ts
[75.02s] fetch
ConnectionRefused: Unable to connect. Is the computer able to access the url?
 path: "http://192.168.1.134/"
// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
console.time('fetch');

try {
  let response = await fetch('http://192.168.1.134', {
    signal: controller.signal,
  });
  // @ts-expect-error abc
} catch (err: Error) {
  console.timeEnd('fetch');
  if (err.name == 'AbortError') {
    // handle abort()
    console.info('Aborted!');
  } else {
    throw err;
  }
}

ImLunaHey avatar Dec 26 '23 09:12 ImLunaHey

~~Ah, if I set timeout: false it works with the abort handler.~~

ignore me this doesnt work it just instantly times out. 😞

➜  stable-diffusion-discord-bot git:(main) ✗ bun run src/file.ts
[13.65ms] fetch
FailedToOpenSocket: Was there a typo in the url or port?
 path: "http://192.168.1.134/"

ImLunaHey avatar Dec 26 '23 09:12 ImLunaHey

I am facing the same issue on AbortSignal.timeout as @uxmaster on the latest bun canary build (1.0.20+05984d405) as well.

See also #7512 and #7513.

I'm in China and the firewall is blocking google.com, in my test code I guess fetch is not handling signal in the DNS resolution stage. I replaced google.com with a URL that is accessible and signal is fine!

drag0n-app avatar Jan 02 '24 08:01 drag0n-app

I am facing the same issue on AbortSignal.timeout as @uxmaster on the latest bun canary build (1.0.20+05984d405) as well. See also #7512 and #7513.

I'm in China and the firewall is blocking google.com, in my test code I guess fetch is not handling signal in the DNS resolution stage. I replaced google.com with a URL that is accessible and signal is fine!

The issue happens on GitHub Actions as well, bun just timeout indefinitely even with AbortSignal.timeout(4000).

SukkaW avatar Jan 02 '24 13:01 SukkaW

Very much needed, please fix this

arthurvanl avatar May 22 '24 09:05 arthurvanl

I ran into this issue trying to hook into Twitter's streaming API. It uses a persistent HTTP connection to continuously stream new tweet data. I was unable to use the AbortController to kill the connection as part of my graceful shutdown.

If anyone is interested, my "solution" was to make the connection in a bun child process, ferry the data back to the main process via IPC, and call proc.kill() when done. :vomiting_face: :vomiting_face:

tlonny avatar Jun 03 '24 09:06 tlonny

Ran into this today as well

	const response = await fetch( url, {
		method: 'HEAD',
		redirect: 'manual',
		signal: AbortSignal.timeout( 3000 ),
	} );

Temporary, dumb workaround:

function fetchWithTimeout( input: RequestInfo, init: RequestInit = {} ): Promise< Response > {
	const { signal, ...rest } = init;

	return new Promise( ( resolve, reject ) => {
		const onAbort = () => reject( new Error( `Fetch aborted due to signal for ${ input }` ) );

		if ( signal ) {
			signal.addEventListener( 'abort', onAbort );
		}

		fetch( input, { ...rest, signal } )
			.then( resolve )
			.catch( reject )
			.finally( () => {
				if ( signal ) {
					signal.removeEventListener( 'abort', onAbort );
				}
			} );
	} );
}

pyronaur avatar Jun 04 '24 08:06 pyronaur

@pyronaur this does work, but I dont want my code to stop from running after an abort. It must continue with the code but not still wait for a response

arthurvanl avatar Jun 12 '24 12:06 arthurvanl

Then wrap it in a try/catch as you would a real fetch

pyronaur avatar Jun 14 '24 05:06 pyronaur

@pyronaur I did that:

image But it doesnt stop from fetching

arthurvanl avatar Jun 14 '24 06:06 arthurvanl

Yep - it doesn't stop from fetching - but at least in my situation, that was enough to allow the program move on without waiting for the resolution. However, the chickens will come home to roost eventually and throw those errors. It ain't pretty at the moment 🤷‍♂️

pyronaur avatar Jun 16 '24 05:06 pyronaur

Yea so still waiting for this to be fixed. Very much needed!

arthurvanl avatar Jun 17 '24 06:06 arthurvanl

This is so random.. Sometimes it works, most times it doesn't.

let controller = new AbortController();

const res = fetch(`http://192.168.0.25:9090`, {
  signal: controller.signal,
});

setTimeout(() => {
  controller.abort();
}, 1000);

res.then((res) => {
  console.log(res.status, res.statusText, res.headers);
});

controller.signal.addEventListener("abort", () => {
  console.log("Aborted");
});

When I start this script over and over again I always get a different behaviour...

FailedToOpenSocket: Was there a typo in the url or port?
 path: "http://192.168.0.25:9090/"

Bun v1.1.17 (macOS arm64)
schettn@Nicos-Mac-mini lens % bun run tes.ts
FailedToOpenSocket: Was there a typo in the url or port?
 path: "http://192.168.0.25:9090/"

Bun v1.1.17 (macOS arm64)
schettn@Nicos-Mac-mini lens % bun run tes.ts
FailedToOpenSocket: Was there a typo in the url or port?
 path: "http://192.168.0.25:9090/"

Bun v1.1.17 (macOS arm64)
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
^[[A^C
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
^C
schettn@Nicos-Mac-mini lens % bun run tes.ts
FailedToOpenSocket: Was there a typo in the url or port?
 path: "http://192.168.0.25:9090/"

Bun v1.1.17 (macOS arm64)
schettn@Nicos-Mac-mini lens % 

schettn avatar Jun 28 '24 07:06 schettn

When I use import fetch from "node-fetch";, the behaviour is similar:

schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
^C
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
schettn@Nicos-Mac-mini lens % bun run tes.ts
Aborted
^C
schettn@Nicos-Mac-mini lens % 

schettn avatar Jun 28 '24 07:06 schettn

That is indeed very odd

arthurvanl avatar Jun 28 '24 07:06 arthurvanl

It depends on the delta when starting the script.

When I restart the script quickly after it exists, it exits again. If I wait some seconds and call it again. It hangs.

schettn avatar Jun 28 '24 07:06 schettn

Without the AbortController:

const res = fetch(`http://192.168.0.25:9090`, {});

res.then((res) => {
  console.log(res.status, res.statusText, res.headers);
});
schettn@Nicos-Mac-mini lens % bun run tes.ts
schettn@Nicos-Mac-mini lens % bun run tes.ts
schettn@Nicos-Mac-mini lens % bun run tes.ts
schettn@Nicos-Mac-mini lens % bun run tes.ts
^[[AFUUUCK%                                                                                                                                                                                     
schettn@Nicos-Mac-mini lens % bun run tes.tsFUUUCK

schettn avatar Jun 28 '24 07:06 schettn

With axios the timeout works and it errors. But sometimes it just hangs after the error...

import axios from "axios";

axios
  .get("http://192.168.0.25:9090", { timeout: 1000 })
  .then((response) => {
    console.log(response.status, response.statusText, response.headers);
  })
  .catch((error) => {
    console.error("Error fetching data:", error.name);
  });
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: AxiosError
^C
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % 
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: FailedToOpenSocket
schettn@Nicos-Mac-mini lens % bun run tes.ts
Error fetching data: AxiosError
HAAAANGS         

schettn avatar Jun 28 '24 08:06 schettn

The page primarily documents the Bun-native Bun.serve API. Bun also implements fetch and the Node.js http and https modules.

Something in the http / https implementation must be wrong. Therefore fetch, node-fetch and axios all fail.

schettn avatar Jun 28 '24 08:06 schettn

I believe this is already fixed in version 1.1.18+5a0b93523 If you are still running in this Issue please reopen with a code to reproduce it.

cirospaciari avatar Jul 10 '24 00:07 cirospaciari

image Does not work

arthurvanl avatar Jul 10 '24 06:07 arthurvanl

@cirospaciari. Any ideas on my comments above? This still does not work using the canary version.

schettn avatar Jul 10 '24 06:07 schettn

image Does not work

I tried to reproduce this. This is my error:

let start = performance.now();

const res = await fetch(
  "https://tools.arthurvan1.nl/api/v1/delay/time?duration=6000",
  {
    method: "GET",
    signal: AbortSignal.timeout(3000),
  }
);

console.log(res);

console.log(performance.now() - start);

image

schettn avatar Jul 10 '24 06:07 schettn

This works now!

const res = await fetch("https://httpstat.us/504?sleep=100000", {
  method: "GET",
  signal: AbortSignal.timeout(3000),
});

schettn avatar Jul 10 '24 06:07 schettn

Tried your example. Did not work for me

arthurvanl avatar Jul 10 '24 07:07 arthurvanl

Which bun version? Mine is Bun v1.1.19-canary.1+af6035ce3 (macOS arm64)

schettn avatar Jul 10 '24 07:07 schettn

1.1.19-canary.1+af6035ce3 on windows

arthurvanl avatar Jul 10 '24 07:07 arthurvanl

As @uxmaster mentioned, my observation is also in the case if, say, 100% of the TCP packets are lost, then the timeout in Bun's fetch never triggers. It looks like some tests mentioned in this issue are having the HTTP server hang for a few seconds. In my experience, that case works as expected, i.e. if the TCP connection is made correctly but the HTTP response is slow to come back, the timeout works as expected. The timeout does not trigger when the TCP connection itself isn't able to get established (excluding the cases where it's a clear "Connection refused" from remote.)

To test, I used "Network Link Conditioner" from Xcode tools to easily toggle the simulation of packet loss and have this simple script:

let start = performance.now();

const interval = setInterval(() => {
  console.log(
    `tick: script has been running for ${performance.now() - start}ms`
  );
}, 100);

await fetch("http://www.google.com", {
  method: "GET",
  signal: AbortSignal.timeout(1000),
});

console.log(`finished fetching after ${performance.now() - start}ms`);
clearInterval(interval);

I ran the script with both Node (22.4.1) and Bun (1.1.19-canary.1+55d59ebf1), toggling the packet loss on and off.

With Node, normal connection vs simulated 100% packet loss show this result:

❯ node test.mjs
tick: script has been running for 101.6055ms
finished fetching after 158.71391599999998ms

❯ node test.mjs
tick: script has been running for 102.055541ms
tick: script has been running for 203.384583ms
tick: script has been running for 304.127458ms
tick: script has been running for 405.789708ms
tick: script has been running for 506.86683300000004ms
tick: script has been running for 607.864541ms
tick: script has been running for 708.853666ms
tick: script has been running for 809.484083ms
tick: script has been running for 910.721958ms

node:internal/deps/undici/undici:13178
      Error.captureStackTrace(err);
            ^
DOMException [TimeoutError]: The operation was aborted due to timeout
    at node:internal/deps/undici/undici:13178:13
    at async file:///Users/jet/test.mjs:9:1

Node.js v22.4.1

On Bun, this is the result with normal network and simulated 100% loss, the timeout is never triggered in the latter case:

❯ bun run test.mjs
tick: script has been running for 101.66695800000001ms
finished fetching after 152.828375ms

❯ bun run test.mjs
tick: script has been running for 101.442458ms
tick: script has been running for 201.491083ms
tick: script has been running for 302.519375ms
tick: script has been running for 403.55649999999997ms
tick: script has been running for 504.58087500000005ms
tick: script has been running for 605.4719580000001ms
tick: script has been running for 705.959292ms
tick: script has been running for 806.545958ms
tick: script has been running for 907.446542ms
tick: script has been running for 1007.5729580000001ms
tick: script has been running for 1108.271542ms
tick: script has been running for 1209.293458ms
tick: script has been running for 1310.1782500000002ms
tick: script has been running for 1410.759208ms
tick: script has been running for 1511.780125ms
tick: script has been running for 1612.88775ms
tick: script has been running for 1713.898917ms
tick: script has been running for 1814.1675420000001ms
tick: script has been running for 1915.19975ms
tick: script has been running for 2015.985417ms
tick: script has been running for 2117.0055ms
tick: script has been running for 2217.9091670000003ms
tick: script has been running for 2318.1357080000002ms
tick: script has been running for 2419.150625ms
tick: script has been running for 2519.794042ms
tick: script has been running for 2620.288042ms
tick: script has been running for 2721.102125ms
tick: script has been running for 2821.647292ms
tick: script has been running for 2922.681292ms
tick: script has been running for 3023.352833ms
tick: script has been running for 3123.504125ms
tick: script has been running for 3224.468ms
tick: script has been running for 3325.488625ms
tick: script has been running for 3426.7003330000002ms
^C

Hope that helps with getting this bug fixed!

jetzhou avatar Jul 10 '24 23:07 jetzhou