Skip to content

usockets: bind the first getaddrinfo result for a named listen host - #47224

Open
robobun wants to merge 2 commits into
mainfrom
robobun/613ede77/listen-resolver-order
Open

robobun wants to merge 2 commits into
mainfrom
robobun/613ede77/listen-resolver-order

Conversation

@robobun

@robobun robobun commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #47172, fixes #15560

Problem

  • listen(port, "localhost") binds ::1 even when the resolver puts 127.0.0.1 first, as the reporter's Windows machine does. Node binds 127.0.0.1, so curl http://127.0.0.1:<port>/ reaches Node and not bun ./index.html.
  • Cause: bsd_create_listen_socket (packages/bun-usockets/src/bsd.c:1360), bsd_create_bound_socket and bsd_create_udp_socket walk every AF_INET6 result before any AF_INET result.
  • The walk hides a port conflict (Bun thinks it can bind two processes to the same port #15560): with ::1:P taken, listen(P, "localhost") binds 127.0.0.1:P in silence.

Fix

  • One helper resolves the host. Every walk binds the first entry in resolver order. With no host it keeps AF_INET6 first, so the default listener stays on :: and still falls back to 0.0.0.0 on any error, as before.
  • For a named host a bind() failure moves to the next entry only for EADDRNOTAVAIL or EAFNOSUPPORT (IPv6 disabled on lo, fix(serve): When IPv6 is not enabled, attempt to bind to IPv4 address under the same hostname. #6533). Every other error is reported.
  • UDP gains the same fallback and no longer leaks the fd when IPV6_V6ONLY fails. socket() errors now reach *error in the TCP walks.
  • Verified: 7 new tests in test/js/bun/http/serve-listen.test.ts, test/js/node/net/node-net-server.test.ts and test/js/node/net/server.spec.ts. 5 fail on the released binary.

Background

Downsides

  • A second listen(P, "localhost") now fails with EADDRINUSE where it used to bind the other loopback family, as in Node.
  • On an IPv4-first host a localhost listener moves from ::1 to 127.0.0.1. A client that connects to [::1] by literal is refused, as with Node.
  • Per listen: socket/bind/listen unchanged at 1/1/1 in both resolver orders (LD_PRELOAD counter, main vs PR). bsd.c -O2 text 11748 -> 11692 bytes.
Notes

Repro on Linux with the reporter's resolver order:

printf 'precedence ::ffff:0:0/96  100\n' > /etc/gai.conf
node listen.mjs   # listen(0,'localhost') -> 127.0.0.1
bun listen.mjs    # listen(0,'localhost') -> ::1, connect 127.0.0.1: ECONNREFUSED

Stock Windows and glibc both put ::1 first for localhost. The reporter's machine prefers IPv4 (a prefix policy or DisabledComponents setting). Node and Bun then disagree, and this change makes them agree. On hosts with the default order nothing moves.

Tests. "every listener binds the first address dns.lookup reports" runs on every platform and pins the contract: Bun.serve, Bun.listen, net.createServer().listen and Bun.udpSocket on localhost bind dns.lookup("localhost", { order: "verbatim" })[0]. Two namespace tests make the order bug deterministic on Linux: one bind-mounts an IPv4-first gai.conf inside unshare -Urm, one disables IPv6 on lo inside unshare -Urn. They skip where user namespaces are off. The EADDRINUSE tests need no namespace and fail on the released binary everywhere both loopback families exist. "falls back to 0.0.0.0 when only [::1]:port is taken" pins the unchanged wildcard fallback (Linux refuses bind(::) there, other platforms allow it). The server.spec.ts test was skipped with "FIXME: hostname is not resolved" and now runs with port 0 and the resolved address.

Before this change Bun.udpSocket({ hostname: "localhost" }) failed with bind EADDRNOTAVAIL when IPv6 was disabled on lo: the UDP walk picked the first AF_INET6 entry and bound once, with no fallback.

The EADDRNOTAVAIL fallthrough is per address, not per family, and is broader than Node: for a host whose first record is not local (a stale AAAA record) Node reports EADDRNOTAVAIL and Bun binds the next record. The TCP walks did that before this change too. UDP now does the same, so the TCP and QUIC halves of one Bun.serve pick the same address.

Wildcard: Bun.serve({ port }) with [::1]:port held still binds 0.0.0.0, as in 1.4.x and as #46522 documents ("without a hostname nothing changes"). Node reports EADDRINUSE there. Only Bun.serve passes a null host to bsd.c; node:net passes the literal "::" (#43201 handles that path) and Bun.listen requires a hostname.

Syscall counts: an LD_PRELOAD shim that counts socket/bind/listen (AF_INET/AF_INET6 only), run on -e scripts for Bun.serve, Bun.listen and Bun.udpSocket with hostname: "localhost" and for the wildcard, on the main and PR debug builds, with the default and the IPv4-first gai.conf. Every cell is 1 socket, 1 bind, 1 listen (0 listen for UDP). strace and bloaty are not installed in this environment. Size is size on bsd.c compiled with clang -O2 from main and from this branch.

dns.setDefaultResultOrder("ipv4first") and --dns-result-order still do not move a node:net listener: the listen path does not go through dns.lookup. That is a separate node-compat gap in src/js/node/net.ts and is not changed here.

Suites run on the debug build: serve-listen.test.ts, server.spec.ts, node-net-server.test.ts, serve.test.ts, udp_socket.test.ts, test/js/node/cluster, node-net.test.ts, node-http.test.ts, localhost-loopback-contract.test.ts, test/js/node/dgram. Pre-existing failures in this container, the same on main: serve.test.ts "root range port" (runs as root) and "abort a sendfile response" (timeout), node-http.test.ts "a dispatch that throws while an earlier response is pending" (timeouts), a few node-net.test.ts subprocess tests (no network, timeouts).

Self-reviewed: 10 concerns raised, 8 addressed (wildcard policy kept and pinned, socket() error reported, comment no longer claims a Windows order, ungated contract test, gai.conf test asserts only what it controls and skips instead of returning, server.spec.ts un-skipped, #36710/#43201/#46522 named). Not taken: narrow the EADDRNOTAVAIL fallthrough to the IPv6 loopback entry (kept the existing per-address behaviour and documented it), and land this as a commit on #46522 instead of a PR (a sequencing call for the maintainer).

bsd_create_listen_socket, bsd_create_bound_socket and bsd_create_udp_socket
walked every AF_INET6 entry before any AF_INET entry, so listen(port,
"localhost") bound ::1 even when the resolver put 127.0.0.1 first. Node
binds the first dns.lookup() result. The three walks now run over the
getaddrinfo chain in order and bind the first entry. The wildcard (no host)
keeps IPv6 first so the default listener stays on the dual-stack ::.

A bind() failure moves to the next entry only when it says the family is
unusable on this host (EADDRNOTAVAIL, EAFNOSUPPORT). EADDRINUSE and EACCES
are reported instead of binding the other loopback family. UDP gains the
same fallback and no longer leaks the fd when IPV6_V6ONLY fails.
… resolver contract in tests

The wildcard (no host) falls through on any bind error as before, so a `::`
bind that fails still gets a 0.0.0.0 listener. Only a named host stops on
EADDRINUSE and EACCES. bsd_create_socket now writes its error for the TCP
walks too, so a run where every socket() fails reports EAFNOSUPPORT.

Tests: every listener binds the first dns.lookup() result on every platform,
the wildcard fallback is pinned, the gai.conf test asserts only the order it
controls and skips when the namespace cannot change it, and the server.spec
hostname test is no longer skipped.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 6c3d1bf and 1126b33.

Files selected for processing: 4
  • packages/bun-usockets/src/bsd.c
  • test/js/bun/http/serve-listen.test.ts
  • test/js/node/net/node-net-server.test.ts
  • test/js/node/net/server.spec.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked two things in the bsd.c rewrite and ruled them out: relinking ai_next in bsd_addrinfo_inet6_first is safe for freeaddrinfo() on musl as well as glibc/Windows (musl counts the nodes in the chain and locates the allocation base from the last node's slot, so a reordered chain that still contains every node frees cleanly), and passing error into bsd_create_socket does not clobber a later bind errno — it zeroes *error on success before the bind result is written, so the reported code is still the last failing call's.

Extended reasoning...

The change rewrites the getaddrinfo walk shared by bsd_create_bound_socket, bsd_create_listen_socket and bsd_create_udp_socket in packages/bun-usockets/src/bsd.c, plus three test files; it touches the native listen/bind path used by Bun.serve, Bun.listen, node:net and Bun.udpSocket but no auth, crypto or input-parsing surface. Inline findings were posted, so a human look is already signalled; this note records only the chain-relink/freeaddrinfo and out-parameter ordering concerns that were examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines +1827 to +1831
last_error = LIBUS_ERR;
bsd_close_socket(listenFd);
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
if (!bsd_bind_should_try_next(host, last_error)) {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Operators running Bun.serve with http3 and no hostname can now get a QUIC listener on 0.0.0.0 while TCP sits on ::, where the base failed or retried. The new UDP walk at bsd.c:1829 falls through on any bind() error when host == NULL, so a UDP [::]:N conflict silently yields an IPv4-only QUIC socket. The retry loop in src/runtime/server/mod.rs:3129-3145 only fires when h3_listener is None, so neither the fresh-port retry nor the "Failed to listen on UDP port {h3_port} for HTTP/3" error runs. Fix: when the wildcard UDP walk cannot bind the same family the TCP listener bound, report the error so mod.rs can retry or throw, e.g. limit the wildcard fallthrough in bsd_create_udp_socket to EAFNOSUPPORT/EADDRNOTAVAIL, or have the H3 bind pass the TCP listener's bound family instead of NULL. [also at: packages/bun-usockets/src/bsd.c:1829 - HTTP/3 users whose wildcard UDP port is already held now get a server whose QUIC half silently lands on 0.0.0.0 while TCP stays on ::, instead of the error the base throws.]

Why this was flagged

Trigger: Bun.serve({ port: 0, http3: true }) with no hostname on Linux while another process holds a UDP socket on an IPv6-specific address at the port the TCP listener just got. The TCP walk binds [::]:N (bsd.c:1432-1441). The H3 app then calls uws_h3_app_listen_with_config with the same null host (src/runtime/server/mod.rs:3118-3128), which reaches bsd_create_udp_socket(NULL, N, ...). bind([::]:N) fails with EADDRINUSE; bsd_bind_should_try_next(NULL, EADDRINUSE) at bsd.c:1829 returns true because host == NULL, so the loop binds 0.0.0.0:N and returns success. On the base branch the old UDP code bound the first AF_INET6 entry once and returned LIBUS_SOCKET_ERROR on that failure, so h3_listener stayed None and mod.rs:3130-3137 closed TCP:N and retried with a fresh port (port 0) or threw "Failed to listen on UDP port {h3_port} for HTTP/3" (mod.rs:3140). After the change h3_listener is Some, the retry and the error are skipped, and the server advertises Alt-Svc on port N while QUIC is reachable only over IPv4.

Verification: When Bun.serve({ http3: true }) has no hostname and another process holds a UDP socket on an IPv6-only address, the new UDP walk silently binds 0.0.0.0:N where the base reported the failure. bsd_bind_should_try_next (bsd.c:1176) is return host == NULL || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL;, so with host == NULL every bind error moves on to the next entry.

Comment on lines +301 to +303
using occupant = Bun.listen({ hostname: "::1", port: 0, socket: { data() {} } });
using server = serve({ port: occupant.port, fetch: () => new Response() });
expect(server.address).toMatchObject({ address: "0.0.0.0", port: occupant.port });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Linux CI runs get an intermittent failure of the new 0.0.0.0 fallback test whenever the kernel hands the ::1 occupant a port that a concurrent test already uses on 127.0.0.1. Bun.listen at serve-listen.test.ts:301 asks for port 0 on ::1; Linux only rejects a candidate port when the bound address matches, and ::1 never matches 127.0.0.1, so P can collide with another file's 127.0.0.1:P server. serve({ port: P }) at :302 then fails on :: (held by ::1) and again on 0.0.0.0 (held by 127.0.0.1:P) and throws EADDRINUSE. Fix: choose P so both loopback families are known free (probe-bind 0.0.0.0:0, release it, then occupy ::1:P), or retry the occupant/serve pair a bounded number of times on EADDRINUSE.

Why this was flagged

Trigger: the test runs on Linux (skipIf(!isLinux || !hasIPv6) at :298) while any other process on the runner holds 127.0.0.1:P or 0.0.0.0:P, where P is the ephemeral port the kernel picks for Bun.listen({ hostname: "::1", port: 0 }) at serve-listen.test.ts:301. Linux's inet_csk_find_open_port only skips a port when inet_bind_conflict finds a socket whose receive address equals the new one; ipv6_rcv_saddr_equal(::1, 127.0.0.1) is false, so a port already bound by an IPv4 listener of a concurrently running test file is a valid pick for ::1. serve({ port: occupant.port }) at :302 passes host NULL; bsd.c:1432-1447 tries :: (conflicts with ::1:P, EADDRINUSE), then 0.0.0.0:P (conflicts with the foreign 127.0.0.1:P, EADDRINUSE), and the test throws before the expect at :303. Bun's CI runs test files in parallel and each file opens several 127.0.0.1:0 servers, so the per-run probability is roughly (concurrent IPv4 listeners)/(ephemeral range), a steady low-rate flake on every Linux lane. The base branch had no such test.

Verification: packages/bun-usockets/src/bsd.c:1432-1447 iterates :: first and then 0.0.0.0:P, which conflicts with the foreign 127.0.0.1:P owner. The loop exhausts and returns LIBUS_SOCKET_ERROR (line 1450), so serve() throws and the assertion at line 303 is never reached. There is no retry, no reservation of 127.0.0.1:P, and no catch around serve(); any EADDRINUSE from the wildcard walk throws and fails the test.

Comment on lines +384 to +405
// A private network namespace with IPv6 turned off on lo: bind(::1) fails
// with EADDRNOTAVAIL and every listener falls back to 127.0.0.1.
const canDisableIPv6 =
isLinux &&
Bun.which("unshare") !== null &&
Bun.spawnSync({
cmd: ["unshare", "-Urn", "sh", "-c", "ip link set lo up && sysctl -qw net.ipv6.conf.lo.disable_ipv6=1"],
stdout: "ignore",
stderr: "ignore",
}).exitCode === 0;

test.skipIf(!canDisableIPv6)("every listener falls back to 127.0.0.1 when IPv6 is disabled on lo", async () => {
const out = await runProbe([
"unshare",
"-Urn",
"sh",
"-c",
'ip link set lo up && sysctl -qw net.ipv6.conf.lo.disable_ipv6=1 && exec "$@"',
"sh",
]);
expectBound(out, "127.0.0.1");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Maintainers get no test for the EADDRNOTAVAIL/EAFNOSUPPORT fallthrough: deleting those two clauses from bsd_bind_should_try_next breaks no test. In the IPv6-disabled namespace glibc and musl apply RFC 6724 rule 1 (unusable destinations last), so getaddrinfo("localhost") returns 127.0.0.1 first and bind(::1) is never attempted; the test at serve-listen.test.ts:395 passes by ordering alone, not as its comment at line 384 says. Fix: a fixture whose first getaddrinfo entry is unbindable, with the order asserted by getent ahosts in the capability probe and expect(out.lookup[0]).toBe("::1") in the test as line 380 does, so removing the clause at bsd.c:1178 fails a test.

Why this was flagged

The capability probe at test/js/bun/http/serve-listen.test.ts:386-393 only checks that sysctl net.ipv6.conf.lo.disable_ipv6=1 succeeds, and the test at lines 395-405 asserts expectBound(out, "127.0.0.1") without asserting out.lookup[0], unlike the gai.conf test at line 380. With disable_ipv6=1 the kernel removes ::1 and its local route from lo, so the UDP connect() glibc uses in its RFC 6724 sort fails for ::1 and getaddrinfo("localhost") returns 127.0.0.1 before ::1; musl's sort does the same. The walks in packages/bun-usockets/src/bsd.c:1369, 1432 and 1802 therefore bind 127.0.0.1 on the first entry and never reach the bind(::1) EADDRNOTAVAIL path. The only code that consumes EADDRNOTAVAIL/EAFNOSUPPORT is bsd_bind_should_try_next at bsd.c:1177-1179; with || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL deleted, every added test still passes. The test still fails on the base binary because the old UDP walk bound the first AF_INET6 entry with no fallback, so it is not vacuous, but the fallthrough clause is unexercised and a later regression of it goes unnoticed.

Verification: nit — triggered whenever the suite runs on Linux glibc/musl. With net.ipv6.conf.lo.disable_ipv6=1 getaddrinfo("localhost") returns 127.0.0.1 first, so the new code never calls bind(::1). The test at test/js/bun/http/serve-listen.test.ts:395-405 asserts only expectBound(out, "127.0.0.1"), so deleting the EADDRNOTAVAIL/EAFNOSUPPORT clauses from bsd_bind_should_try_next breaks no test.

Comment on lines +1443 to +1444
bsd_close_socket(listenFd);
if (!bsd_bind_should_try_next(host, *error)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Users whose first loopback entry fails bind() for any reason other than the two allowed errnos now get a listen error where the base silently bound the next family. bsd_bind_should_try_next at bsd.c:1180 lets a named host move on only for EAFNOSUPPORT or EADDRNOTAVAIL; every other code stops the walk at bsd.c:1444 and bsd.c:1391. The new *error writes at bsd.c:1251 and bsd.c:1268 feed that check too, so a failing IPV6_V6ONLY setsockopt or bsd_set_reuse on the AF_INET6 entry now ends the walk. Fix: stop the walk only for the port-conflict codes (EADDRINUSE, and WSAEACCES on Windows) and keep trying the remaining entries for everything else, so sandbox and setsockopt failures on one family still reach the other, as on the base.

Why this was flagged

Trigger: listen(port, "localhost") where getaddrinfo lists ::1 first and the ::1 attempt fails with an errno outside {EAFNOSUPPORT, EADDRNOTAVAIL}. Examples reachable from Bun.listen/Bun.serve/node:net: a macOS sandbox-exec profile that allows network-bind only for ip4 (bind on the AF_INET6 socket returns EPERM), or a container whose IPV6_V6ONLY setsockopt fails at bsd.c:1267. On the base, bsd_create_listen_socket closed the fd and went on to the AF_INET loop on any bsd_bind_listen_fd failure, and the IPV6_V6ONLY path returned without writing *error, so 127.0.0.1 was bound silently. The new code sets *error at bsd.c:1251/1268/1094, then bsd_bind_should_try_next(host, *error) at bsd.c:1444 returns 0 for EPERM/EACCES/ENOPROTOOPT and the loop breaks; the same break exists at bsd.c:1391 for the cluster path. The caller gets a thrown listen error (Listener.rs:538) or Bun.serve's errno-based error instead of a working 127.0.0.1 listener. Remedy: stop only on EADDRINUSE (and WSAEACCES on Windows), continue on all other errors.

Verification: bsd_bind_should_try_next at bsd.c:1177-1179 is host == NULL || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL. In bsd_create_listen_socket the walk does if (!bsd_bind_should_try_next(host, *error)) { break; } at bsd.c:1443-1446. On the base, the AF_INET6 loop fell through to the AF_INET loop unconditionally, so any ::1 failure previously ended with a working 127.0.0.1 listener; now the listen call reports the error.

Comment on lines +351 to +353
test("every listener binds the first address dns.lookup reports", async () => {
const out = await runProbe([]);
expectBound(out, out.lookup[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) macOS CI gets a failing ungated test on any Mac whose libc getaddrinfo does not put ::1 first for localhost, because the test's reference order comes from a different resolver than the listen path. dns.lookup on macOS goes to dns_sd (dns.rs:5141-5144) and take_results force-sorts AF_INET6 first at dns_sd.rs:306-309; the listeners bind libc getaddrinfo order via bsd.c:1150. The assertion at serve-listen.test.ts:353 compares the two and has no platform gate. Fix: derive the expected address from the same resolver the listen path uses (a libc getaddrinfo probe, or skip the test where dns.lookup and getaddrinfo are known to differ), so the contract test only asserts what it controls on every platform.

Why this was flagged

Trigger: the test at serve-listen.test.ts:351 runs on macOS (no skipIf) and the Mac's libinfo getaddrinfo("localhost", AI_PASSIVE) returns 127.0.0.1 before ::1. out.lookup at :312 is produced by dns_sd::lookup (dns.rs:5144), whose take_results sorts every AF_INET6 answer ahead of AF_INET at dns_sd.rs:307-309 regardless of system order, so out.lookup[0] is ::1 whenever an AAAA arrives. The four listeners in the probe bind bsd_listen_getaddrinfo's first entry (bsd.c:1150, resolver order for a named host), which is 127.0.0.1 on such a Mac. expectBound(out, out.lookup[0]) at :353 then fails on serve/listen/net/udp. The base branch had no such test. The test is ungated and the two resolvers are independent code paths, so any divergence on a CI Mac fails the suite. Remedy: compute the expected address with the same resolver as the listen path or gate the test on macOS.

Verification: Triggered on any macOS host where libinfo getaddrinfo("localhost", AI_PASSIVE) does not return ::1 first. The test at serve-listen.test.ts:351-353 runs ungated. On macOS the lookup is served by dns_sd::lookup (dns.rs:5141-5144) and take_results force-sorts AF_INET6 ahead of AF_INET (dns_sd.rs:306-309). The listeners bind the first entry of libc getaddrinfo() in bsd_listen_getaddrinfo (bsd.c:1158).

Comment on lines +359 to +360
const gaiConf = join(tmpdirSync(), "gai.conf");
writeFileSync(gaiConf, "precedence ::ffff:0:0/96 100\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): the new gai.conf fixture is created with tmpdirSync plus a module-scope writeFileSync, which the root CLAUDE.md (line 101) says not to use; tests should use tempDir from "harness". Fix: build the fixture with tempDir("gai", { "gai.conf": "precedence ::ffff:0:0/96 100\n" }) and pass join(String(dir), "gai.conf") to the unshare probe and the test, so the directory is created and disposed through the harness like the other tests in the suite. [also at: test/js/bun/http/serve-listen.test.ts:358 - nit: the new gai.conf fixture is created with tmpdirSync plus writeFileSync at describe scope, which the root CLAUDE.md asks tests not to do, and it runs on every platform even though only the Linux unshare test reads it.]

Why this was flagged

test/js/bun/http/serve-listen.test.ts:359 calls tmpdirSync() and test/js/bun/http/serve-listen.test.ts:360 writes gai.conf into it at describe-body scope. CLAUDE.md line 101 states: use tempDir from harness, do not use tmpdirSync or fs.mkdtempSync to create temporary directories. The file already had one pre-existing tmpdirSync use at line 13, but the diff adds a second, so the convention slip is widened rather than fixed. Consequence is maintainability only: the directory is never cleaned up and the test deviates from the documented harness pattern; no runtime behaviour differs from the base branch.

Verification: nit. Triggering condition: whenever the file is loaded (the fixture is built unconditionally at describe-body scope). The diff adds const gaiConf = join(tmpdirSync(), "gai.conf"); at /home/claude/bun/test/js/bun/http/serve-listen.test.ts:359. /home/claude/bun/CLAUDE.md:101 states: "Do not use tmpdirSync or fs.mkdtempSync to create temporary directories." Nothing functionally breaks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants