Compare commits

...

3 Commits

Author SHA1 Message Date
Jarred Sumner
c1eccb94a4 Merge branch 'main' into claude/fix-ipv6-connect-timeout 2026-02-23 17:39:13 -08:00
Claude Bot
a9a0533998 fix(net): reduce connection attempt timeout from 10s to 1s
Per review feedback, reduce the Happy Eyeballs connection attempt timeout
from 10 seconds to 1 second. With 4s timer granularity this fires within
~4-8s, which is still fast enough to avoid long hangs on broken IPv6.

Also increase the test elapsed-time threshold to 20s and remove the
explicit test timeout to avoid flakiness in CI.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 06:00:32 +00:00
Claude Bot
23a22ab1dc fix(net): add connection timeout to prevent IPv6 hangs in bun update
When DNS returns both IPv6 and IPv4 addresses (common for npm registries),
broken IPv6 connectivity causes connection attempts to hang indefinitely.
The TCP stack's default SYN timeout is typically 120+ seconds per attempt,
and without a connection-level timeout, `bun update` and other commands
freeze waiting for unreachable IPv6 addresses.

This adds a 10-second connection attempt timeout to sockets created during
multi-address DNS connection establishment (the `start_connections` path in
uSockets). When a connecting socket times out, the timer sweep now treats
it as a connection failure and triggers the existing fallback logic to try
the next address in the list. This implements a simplified Happy Eyeballs
(RFC 8305) approach.

Changes:
- Set 10s timeout on individual sockets in `start_connections()` instead
  of inheriting the disabled (255) timeout from the connecting socket
- In the timer sweep, detect timed-out connecting sockets (those with
  non-null `connect_state`) and call `us_internal_socket_after_open()`
  with ETIMEDOUT to trigger address fallback

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 01:04:49 +00:00
3 changed files with 64 additions and 2 deletions

View File

@@ -591,7 +591,13 @@ int start_connections(struct us_connecting_socket_t *c, int count) {
struct us_socket_context_t* context = c->context;
struct us_socket_t *s = (struct us_socket_t *)us_create_poll(loop, 0, sizeof(struct us_socket_t) + c->socket_ext_size);
s->context = context;
s->timeout = c->timeout;
/* Set a connection attempt timeout so we don't hang forever on unreachable
* addresses (e.g. broken IPv6 connectivity). With the 4s timer granularity,
* this fires within ~4-8s. This implements a simplified Happy Eyeballs
* (RFC 8305) approach: if a connection attempt doesn't succeed within this
* window, it will be treated as a connect error and the next address in
* the list will be tried. */
us_socket_timeout(0, s, 1);
s->long_timeout = c->long_timeout;
struct us_socket_flags* flags = &s->flags;
flags->low_prio_state = 0;

View File

@@ -158,7 +158,15 @@ void us_internal_timer_sweep(struct us_loop_t *loop) {
if (short_ticks == s->timeout) {
s->timeout = 255;
if (context->on_socket_timeout != NULL) context->on_socket_timeout(s);
/* If this socket is still connecting (has a connect_state), treat the
* timeout as a connection failure. This implements Happy Eyeballs-style
* fallback: if an address (e.g. IPv6) doesn't respond within the
* connection timeout, we fail it and try the next address. */
if (s->connect_state != NULL) {
us_internal_socket_after_open(s, ETIMEDOUT);
} else {
if (context->on_socket_timeout != NULL) context->on_socket_timeout(s);
}
}
if (context->iterator == s && long_ticks == s->long_timeout) {

View File

@@ -0,0 +1,48 @@
import { expect, test } from "bun:test";
// This test verifies that connection attempts via the multi-address DNS path
// don't hang forever. The connection timeout in the socket layer should
// cause individual connection attempts to fail within a bounded time,
// implementing a simplified Happy Eyeballs (RFC 8305) approach.
//
// This is critical for `bun update` and other package manager operations
// where IPv6 addresses may be returned by DNS but IPv6 connectivity is
// broken, causing connections to hang indefinitely.
test("fetch to IPv4 server via localhost succeeds", async () => {
// Start a server only on IPv4 127.0.0.1
await using server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch() {
return new Response("ok");
},
});
// Connect directly to the IPv4 address - this should work immediately
const resp = await fetch(`http://127.0.0.1:${server.port}/`);
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("ok");
});
test("fetch via localhost resolves when server is on 127.0.0.1", async () => {
// Start a server only on IPv4 127.0.0.1
// "localhost" may resolve to both ::1 and 127.0.0.1
// The connection to ::1 should fail quickly and fall back to 127.0.0.1
await using server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch() {
return new Response("localhost-ok");
},
});
const start = performance.now();
const resp = await fetch(`http://localhost:${server.port}/`);
const elapsed = performance.now() - start;
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("localhost-ok");
// With a 1s connect timeout and 4s timer granularity, fallback fires in ~4-8s.
// Use 20s threshold to avoid flakiness from timer jitter and CI load.
expect(elapsed).toBeLessThan(20_000);
});