Files
bun.sh/test/js/bun/io/fetch/fetch-abort-slow-connect.test.ts
Jarred Sumner 4dfd87a302 Fix aborting fetch() calls while the socket is connecting. Fix a thread-safety issue involving redirects and AbortSignal. (#22842)
### What does this PR do?

When we added "happy eyeballs" support to fetch(), it meant that
`onOpen` would not be called potentially for awhile. If the AbortSignal
is aborted between `connect()` and the socket becoming
readable/writable, then we would delay closing the connection until the
connection opens. Fixing that fixes #18536.

Separately, the `isHTTPS()` function used in abort and in request body
streams was not thread safe. This caused a crash when many redirects
happen simultaneously while either AbortSignal or request body messages
are in-flight.
This PR fixes https://github.com/oven-sh/bun/issues/14137



### How did you verify your code works?

There are tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
2025-09-25 16:08:06 -07:00

60 lines
1.9 KiB
TypeScript

import { expect, test } from "bun:test";
test.concurrent("fetch aborts when connect() returns EINPROGRESS but never completes", async () => {
// Use TEST-NET-1 (192.0.2.0/24) from RFC 5737
// These IPs are reserved for documentation and testing.
// Connecting to them will cause connect() to return EINPROGRESS
// but the connection will never complete because there's no route.
const nonRoutableIP = "192.0.2.1";
const port = 80;
const start = performance.now();
try {
await fetch(`http://${nonRoutableIP}:${port}/`, {
signal: AbortSignal.timeout(50),
});
expect.unreachable("Fetch should have aborted");
} catch (e: any) {
const elapsed = performance.now() - start;
expect(e.name).toBe("TimeoutError");
expect(elapsed).toBeLessThan(1000); // But not more than 1000ms
}
});
test.concurrent("fetch aborts immediately during EINPROGRESS connect", async () => {
const nonRoutableIP = "192.0.2.1";
const port = 80;
// Start the fetch
const fetchPromise = fetch(`http://${nonRoutableIP}:${port}/`, {
signal: AbortSignal.timeout(1),
});
const start = performance.now();
try {
await fetchPromise;
expect.unreachable("Fetch should have aborted");
} catch (e: any) {
const elapsed = performance.now() - start;
expect(e.name).toBe("TimeoutError");
expect(elapsed).toBeLessThan(1000); // Should reject very quickly after abort
}
});
test.concurrent("pre-aborted signal prevents connection attempt", async () => {
const nonRoutableIP = "192.0.2.1";
const port = 80;
const start = performance.now();
try {
await fetch(`http://${nonRoutableIP}:${port}/`, {
signal: AbortSignal.abort(),
});
expect.unreachable("Fetch should have aborted");
} catch (e: any) {
const elapsed = performance.now() - start;
expect(e.name).toBe("AbortError");
expect(elapsed).toBeLessThan(10); // Should fail immediately
}
});