Skip to content

Report an unresolvable listen hostname as a getaddrinfo error - #37690

Open
robobun wants to merge 1 commit into
mainfrom
farm/d765894c/listen-getaddrinfo-error
Open

robobun wants to merge 1 commit into
mainfrom
farm/d765894c/listen-getaddrinfo-error

Conversation

@robobun

@robobun robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.serve on a hostname that does not resolve throws the wrong error. On Linux it is whatever errno the resolver left behind, blamed on listen (EAGAIN, ENOENT or EMSGSIZE, varying with the input). On macOS and Windows it is Failed to start server. Is port 0 in use? with code: "EADDRINUSE" (the something.localhost report in Bun.serve failed to start on localhost subdomain #25765).
  • Bun.listen, and so net and tls servers, throw a bare Error: Failed to listen at <host> with no code. http and https servers emit the stale errno error.
  • Cause: the usockets listen path reports a failed getaddrinfo without recording why. getaddrinfo returns its error instead of setting errno, so Bun.serve reads a stale errno and Bun.listen reads an out-param that was never written.

Fix

  • The listen path gets a second out-param, dns_error, carrying the raw getaddrinfo code up to the listen callback. Bun.serve and Bun.listen map it with the helpers Bun.connect and fetch already use, so both throw getaddrinfo ENOTFOUND <host> with code, syscall and hostname, the shape Node's server.listen() and Bun's connect side already produce.
  • It is a separate parameter rather than a value in error because the two number spaces overlap (glibc EAI_NONAME is -2, macOS's is 8, Windows returns WSA codes), the same reason the connect path tags error_is_dns.
  • Existing errno paths (EACCES, epoll ENOSPC, EADDRINUSE, unix sockets) are untouched: dns_error is written only when getaddrinfo itself fails, before any socket call, and zero maps to no error. Bun.udpSocket (HTTP/3-only servers) encodes the same failure differently and is left for a separate change.
  • Verification: nine new tests (Bun.serve http/https, Bun.listen tcp/tls, net/tls/http/https server.listen()) fail on the unpatched build and pass with this change. They use a 64-byte DNS label, rejected locally, so no network is involved.

Background

  • getaddrinfo(3) is the libc call that turns a hostname into socket addresses. It reports failure through its EAI_* return value and does not set errno, so errno after a failed call is leftover resolver state. EAI_* numbering differs per platform.
  • usockets (packages/bun-usockets) is Bun's C socket layer; uWS (packages/bun-uws) is the HTTP layer on it. Bun.serve reaches usockets through uWS and a C shim, Bun.listen calls usockets directly, hence two Rust call sites plus the plumbing between.
  • init_eai converts a platform EAI_* code into Bun's DNS error (ENOTFOUND for EAI_NONAME/EAI_NODATA, and for any failure on Windows) and returns None for 0. system_error_with_syscall_and_hostname builds the Node-shaped error with code, syscall and hostname.
  • Node's net, tls, http and https servers resolve the host with dns.lookup before binding, so on an unresolvable host they emit the lookup error itself.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/http/serve-listen.test.ts test/js/node/net/node-net-server.test.ts

Original description

Repro

bun -e 'try { Bun.serve({ hostname: "not.a.real.host.invalid", port: 0, fetch() {} }) } catch (e) { console.log(e.code, e.message) }'

On Linux this prints a different error depending on what the resolver happened to do internally:

EAGAIN EAGAIN: resource temporarily unavailable, listen      # not.a.real.host.invalid
ENOENT ENOENT: no such file or directory, listen             # hostname: "[zz::1::zz]"
EMSGSIZE EMSGSIZE: message too long, listen                  # hostname: "a".repeat(1100)

On macOS and Windows the same call throws Failed to start server. Is port 0 in use? with code: "EADDRINUSE" (the something.localhost report in #25765 is this). Bun.listen with the same hostname, and therefore net/tls servers, throw a bare Error: Failed to listen at <host> with no code at all; http/https servers emit the stale errno error above.

Cause

bsd_create_listen_socket() in packages/bun-usockets/src/bsd.c returns LIBUS_SOCKET_ERROR when getaddrinfo() fails without recording why. getaddrinfo reports failure through its return value, not errno, so at that point errno is whatever glibc's resolver last left behind. Bun.serve's on_listen_failed (src/runtime/server/mod.rs) reads errno on Linux and turns it into a listen error; the other platforms and Bun.listen (src/runtime/socket/Listener.rs, which reads the *error out-param that was never written) fall through to their generic messages.

Fix

bsd_create_listen_socket / us_socket_group_listen gain a second out-param, dns_error, which receives the raw getaddrinfo return code. It is a separate parameter rather than a value in *error because the two number spaces overlap (glibc's EAI_NONAME is -2, macOS's is 8, Windows returns WSA codes), the same reason the connect path tags us_connecting_socket_t::error_is_dns. uWS::TemplatedApp::listen and the uws_listen_handler C shim pass it along to the listen callback, so Bun.serve gets it directly instead of inferring anything from errno.

Bun.serve and Bun.listen map it with c_ares::Error::init_eai and system_error_with_syscall_and_hostname, the same helpers Bun.connect and fetch already use for a failed lookup. Bun.serve (http and https, including the TCP side of http3: true servers), Bun.listen, and everything built on them (net, tls, http, https, http2 servers, bun ./index.html --host, --inspect) now produce:

Error: getaddrinfo ENOTFOUND not.a.real.host.invalid
  code: "ENOTFOUND", syscall: "getaddrinfo", hostname: "not.a.real.host.invalid"

This is the shape Node produces for server.listen() on a host that does not resolve (net, tls, http and https all go through dns.lookup there), and it is what Bun already produces for the connect side of the same failure, so the listen side is now consistent with both. The code mapping (ENOTFOUND for EAI_NONAME/EAI_NODATA, and on Windows for any failure; c-ares numbering in errno) is init_eai's and is shared with Bun.connect/fetch, not introduced here.

The existing errno paths (EACCES, the epoll ENOSPC case, EADDRINUSE, unix sockets) are untouched: dns_error is only written when getaddrinfo itself fails, before any socket call, and init_eai(0) is None on every platform, so on_listen_failed and Listener::listen only take the new branch in that case.

Out of scope, same family: Bun.udpSocket (and so HTTP/3-only servers with http1: false) go through bsd_create_udp_socket, which encodes the getaddrinfo code differently (bind ENOENT <host> today); that is a separate consumer and is left for its own change. A bracketed hostname longer than 1024 bytes crashes Bun.serve before reaching any of this; that is #37631, which is why the tests below exercise brackets and an over-long name separately.

Verification

New tests use a 64-byte DNS label, which every resolver rejects locally, so they do not touch the network:

  • test/js/bun/http/serve-listen.test.ts: Bun.serve http and https, plus the three kinds of input above producing the same error, plus a later Bun.serve still working.
  • test/js/bun/net/socket-dns-error.test.ts: Bun.listen tcp and tls, repeated three times.
  • test/js/node/net/node-net-server.test.ts: net, tls, http and https server.listen() emit the resolver error (net/tls use Bun.listen, http/https use Bun.serve).

With USE_SYSTEM_BUN=1 the nine new tests fail (stale errno or missing code); with this change they pass. serve.test.ts, serve-epoll-add-fail.test.ts (which pins the errno channels this change leaves alone), serve-http3.test.ts, socket.test.ts, node-net.test.ts and the Node test-net-*listen* / bind-twice tests show no new failures compared to the unpatched build. Full CI was green on the first revision; later revisions only changed comments.

Noticed while working on #37631.

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:51 AM PT - Aug 12th, 2026

✅ @robobun, your commit 6e2cc0ceb2211a4e8941650bd51d9517c39e84e9 passed in Build #93073! 🎉


🧪   To try this PR locally:

bunx bun-pr 37690

That installs a local version of the PR into your bun-37690 executable, so you can run:

bun-37690 --bun

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f98a610-c5c2-4731-8c71-10a07e2f90a0

📥 Commits

Reviewing files that changed from the base of the PR and between 9a543cc and 6e2cc0c.

📒 Files selected for processing (15)
  • packages/bun-usockets/src/bsd.c
  • packages/bun-usockets/src/context.c
  • packages/bun-usockets/src/internal/networking/bsd.h
  • packages/bun-usockets/src/libusockets.h
  • packages/bun-uws/src/App.h
  • packages/bun-uws/src/HttpContext.h
  • src/runtime/server/mod.rs
  • src/runtime/socket/Listener.rs
  • src/uws_sys/App.rs
  • src/uws_sys/SocketGroup.rs
  • src/uws_sys/_libusockets.h
  • src/uws_sys/libuwsockets.cpp
  • test/js/bun/http/serve-listen.test.ts
  • test/js/bun/net/socket-dns-error.test.ts
  • test/js/node/net/node-net-server.test.ts

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

@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.

I reviewed this PR and didn't find any bugs. Because it threads an FFI ABI change (uws_listen_handler gains a parameter) through four layers (usockets C → uWS C++ → C shim → Rust) and refactors the TemplatedApp::listen overloads, a human look would still be worthwhile.

What was reviewed:

  • All call sites of bsd_create_listen_socket, us_socket_group_listen, SocketGroup::listen, and uws_listen_handler are updated in lockstep; the h3 listen path uses a separate closure signature and is unaffected.
  • c_ares::Error::init_eai(0) returns None on all platforms, so the existing errno paths (EACCES/ENOSPC/EADDRINUSE, unix sockets) in on_listen_failed and Listener::listen still fire; on Windows the raw WSA getaddrinfo code hits init_eai's catch-all ENOTFOUND arm.
  • The four collapsed TemplatedApp::listen overloads preserve the empty-host → nullptr mapping and the httpContext == nullptr guard (dnsError stays 0).
Extended reasoning...

Overview

This PR fixes error reporting when Bun.serve / Bun.listen (and therefore node net/tls/http/https servers) are given a hostname that fails to resolve. Previously the error was whatever stale errno the resolver happened to leave behind (EAGAIN/ENOENT/EMSGSIZE on Linux) or a generic EADDRINUSE / code-less "Failed to listen" elsewhere. The fix threads a new dns_error out-parameter carrying the raw getaddrinfo(3) return code from bsd_create_listen_socket up through us_socket_group_listen, HttpContext::listen, TemplatedApp::listen, the uws_listen_handler C shim, and into the Rust on_listen / on_listen_failed callbacks, where it is mapped through the same c_ares::Error::init_eai + system_error_with_syscall_and_hostname helpers the connect/fetch/dns paths already use.

The App.h change also collapses four near-identical listen overloads into a shared listenTcp helper. Nine new tests cover Bun.serve (http/https), Bun.listen (tcp/tls), and node net/tls/http/https servers, all using an over-length DNS label so no network is contacted.

Security risks

None identified. This is error-reporting plumbing on a failure path; no new user input reaches allocation, no bounds arithmetic, no auth/crypto/permission logic.

Level of scrutiny

High. The change is well-contained conceptually but spans an FFI callback signature change coordinated across C, C++, a C-ABI shim, and Rust extern declarations — a mismatch anywhere is silent stack corruption. It also touches the Bun.serve and Bun.listen error-handling paths, which are production-critical, and refactors uWS TemplatedApp::listen overloads. I traced every declaration/definition/caller of the affected symbols and found them consistent, and verified the App.h refactor preserves the empty-host and null-httpContext behaviors, but a maintainer familiar with the usockets/uWS layering should confirm.

Other factors

  • init_eai(0) returns None on both the Windows and POSIX branches, so the new DNS branch is only taken when getaddrinfo actually failed; the existing Linux errno special-cases (EACCES, epoll ENOSPC) and the fallback EADDRINUSE message are untouched.
  • On Windows, bsd_create_listen_socket calls native getaddrinfo (returning WSA codes, not UV_EAI_*); the Windows arm of init_eai has a catch-all _ => Some(Error::ENOTFOUND) for non-zero codes, so the tests' expected ENOTFOUND holds there too.
  • The Listener.rs DNS branch is inside the existing cleanup scopeguard, so the half-built listener is torn down on the new early return.
  • The unix-socket listen paths (us_socket_group_listen_unix, uws_listen_domain_handler, on_listen_unix trampoline) correctly do not carry dns_error; the trampoline forwards a hardcoded 0.
  • Test coverage is thorough (both native paths × both TLS variants × node compat surface), hermetic (RFC 1035 over-length label), and asserts exact error shape rather than just "throws".

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is green on the current revision (build 93041, all lanes; build 92679 on the first revision was green too), all review threads are resolved, and the automated reviews found nothing to change.

Reproduced on Linux with the release build (USE_SYSTEM_BUN=1): Bun.serve({ hostname: "not.a.real.host.invalid" }) throws EAGAIN ... listen, "[zz::1::zz]" throws ENOENT, a 1100-byte name throws EMSGSIZE; Bun.listen and net/tls servers produce a code-less Failed to listen at <host>, http/https servers emit the stale errno error. With this branch every one of them reports getaddrinfo ENOTFOUND <host> (code, syscall, hostname set).

The new tests in serve-listen.test.ts, socket-dns-error.test.ts and node-net-server.test.ts fail on the release build and pass with the fix; serve-epoll-add-fail.test.ts still passes, so the existing errno paths are unchanged.

Comment thread src/runtime/server/mod.rs Outdated
Comment thread src/runtime/server/mod.rs Outdated
Comment thread src/runtime/socket/Listener.rs Outdated
Comment thread src/uws_sys/App.rs Outdated
Comment thread src/uws_sys/SocketGroup.rs Outdated
Comment thread src/uws_sys/_libusockets.h Outdated
@robobun
robobun force-pushed the farm/d765894c/listen-getaddrinfo-error branch from 228bb23 to 8101485 Compare August 12, 2026 10:28
Comment thread src/uws_sys/SocketGroup.rs Outdated
bsd_create_listen_socket() returned LIBUS_SOCKET_ERROR when getaddrinfo()
failed without recording why: getaddrinfo reports failures through its
return value, so errno was left holding whatever the resolver last set.
Bun.serve on Linux then built its error from that stale errno (EAGAIN,
ENOENT, EMSGSIZE, depending on the input) attributed to listen(2),
Bun.serve elsewhere claimed the port was in use, and Bun.listen (so also
net/tls servers) threw a code-less "Failed to listen at <host>".

Carry the getaddrinfo return code out of usockets in its own out-param,
separate from the errno one because the two number spaces overlap, and
pass it to the uws listen callback. Bun.serve and Bun.listen map it the
same way Bun.connect and fetch already do, so the TCP listen APIs (and
the node servers built on them) report `getaddrinfo ENOTFOUND <host>`
with code, syscall and hostname set, which is also the error Node emits
for server.listen() on a host that does not resolve. The UDP bind path
(Bun.udpSocket, HTTP/3-only servers) has its own encoding of the same
failure and is left for a separate change.
@robobun
robobun force-pushed the farm/d765894c/listen-getaddrinfo-error branch from 8101485 to 6e2cc0c Compare August 12, 2026 10:34

@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.

I reviewed this PR and didn't find any bugs. Because it threads an FFI ABI change (the uws_listen_handler callback signature and us_socket_group_listen out-param) through C, C++, and Rust and refactors the TemplatedApp::listen overloads, a human look at the plumbing would still be worthwhile.

Checked: the new dns_error param is zero-initialized at every caller (App.h listenTcp, Listener::listen) and only written on getaddrinfo failure, so init_eai(0) → None keeps the existing errno paths (EACCES/ENOSPC/EADDRINUSE/unix) untouched. The Rust on_listen trampoline signature matches the updated C typedef, and on_listen_unix correctly forwards 0. The App.h refactor preserves the empty-host → nullptr fallthrough. Tests are hermetic (64-byte DNS label rejected locally) and cover both native paths (Bun.serve and Bun.listen).

Extended reasoning...

Overview

This PR fixes error reporting for Bun.serve/Bun.listen (and the net/tls/http/https servers built on them) when the listen hostname fails to resolve. Previously the failure surfaced as a stale errno (EAGAIN/ENOENT/EMSGSIZE on Linux) or a generic EADDRINUSE/"Failed to listen" on other platforms. The fix threads the raw getaddrinfo return code through a new dns_error out-parameter from bsd_create_listen_socket (bsd.c) → us_socket_group_listen (context.c/libusockets.h) → HttpContext::listen/TemplatedApp::listen (App.h) → the uws_listen_handler C shim (libuwsockets.cpp/_libusockets.h) → Rust's SocketGroup::listen/App::listen and the on_listen trampoline → on_listen_failed (server/mod.rs) and Listener::listen (Listener.rs), which map it via the existing c_ares::Error::init_eai + system_error_with_syscall_and_hostname helpers to a Node-compatible getaddrinfo ENOTFOUND <host> error. Nine new tests cover http/https (Bun.serve), tcp/tls (Bun.listen), and net/tls/http/https (node compat) with a hermetic 64-byte DNS label.

Security risks

None identified. The change only affects error-reporting on the listen-failure path; no new user input reaches parsing or allocation, and the getaddrinfo code is passed through a fixed mapping table already used by the connect path.

Level of scrutiny

High. This is a cross-language FFI change: it alters a C-ABI callback signature (uws_listen_handler gains an int parameter) and a public C function signature (us_socket_group_listen gains a nonnull out-param), refactors four TemplatedApp::listen C++ overloads into a shared listenTcp helper, and touches the core listen path for both Bun.serve and Bun.listen. An ABI mismatch between the Rust trampoline and the C typedef would only surface at runtime. While I verified the signatures line up and the refactor preserves behavior (empty host → nullptr, options default 0), this is exactly the kind of change where a second pair of eyes on the FFI plumbing is warranted.

Other factors

CI was green on the first revision (build 92679, all lanes); subsequent pushes only trimmed comments per the comment-cop bot, and all those threads are resolved. The tests are well-constructed: hermetic (over-long DNS label rejected locally, no network), exercise both native paths and both TLS variants, verify a subsequent listen still works after a failure, and destructure the error object to assert exact code/syscall/hostname/message rather than just toThrow(). I confirmed init_eai(0) returns None on every platform (including the Windows fallthrough), so the new branch cannot fire when getaddrinfo succeeded. The _hostname → hostname rename in on_listen_failed is because the field is now read outside the Linux-only cfg block.

@robobun

robobun commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Another report of the same bug came in from the work on #32628. net.Server#listen(port, host) on a host that does not resolve emits Error: Failed to listen at <host> with no code and no syscall. Node emits getaddrinfo EAI_AGAIN <host> (or ENOTFOUND) with code, syscall and hostname. This PR covers that case, and the net row of the new test in test/js/node/net/node-net-server.test.ts checks it.

The bug is still present on main at db059d8. bsd_create_listen_socket in packages/bun-usockets/src/bsd.c still drops the getaddrinfo return value.

This branch now conflicts with main in packages/bun-usockets/src/bsd.c and src/uws_sys/SocketGroup.rs. The conflicts come from the dead code removals in #39732 and #39795, not from a competing fix. A rebase is enough.

@robobun

robobun commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up from #40986, which adds a bun_dns::is_valid_hostname check to Bun.serve, Bun.listen and Bun.udpSocket before the synchronous getaddrinfo (the bind-side counterpart of #40970).

If #40986 lands first, the 64-byte label these tests use is answered in-process with getaddrinfo ENOTFOUND <host> and never reaches the dns_error plumbing. The assertions still pass, but they no longer fail without this change. A well-formed name that glibc rejects locally keeps the test hermetic on Linux: a*b.com, a/b.com, -a.com and ünicode.com all return EAI_NONAME without a query, and is_valid_hostname accepts all four. macOS and Windows need a check in CI.

The two changes are independent otherwise: #40986 does not touch bsd.c, context.c or the uWS headers, and the nxdomain.invalid case (and #25765) is still this PR's.

Jarred-Sumner pushed a commit that referenced this pull request Aug 30, 2026
#40986)

### Problem
- `Bun.serve`, `Bun.listen` and `Bun.udpSocket` (bind and connect)
resolve their hostname with a synchronous `getaddrinfo` in uSockets
(`packages/bun-usockets/src/bsd.c:1340`, `:1733`, `:1818`), even for a
name that can never be a hostname. Some resolvers never answer such a
query, and the call blocks the JS thread (#40970 saw this on the connect
side).
- Its error is also wrong: `Bun.serve({ hostname: "this is not a
hostname" })` throws `ENOENT: no such file or directory, listen`,
`Bun.listen` a bare `Failed to listen at <host>`, `Bun.udpSocket` `bind
ENOENT <host>`, and `dgram`'s `connect()` reports success.

### Fix
- Each path checks the hostname with `bun_dns::is_valid_hostname` before
it calls into uSockets. A rejected name throws `getaddrinfo ENOTFOUND
<host>` with `code`, `syscall` and `hostname`: what `Bun.connect`
reports for it and what Node emits from `server.listen()`.
- `Bun.serve` now strips brackets only around an IPv6 literal: `"[::1]"`
still binds, `"[foo]"` no longer resolves as `foo`.
- A well-formed name that does not resolve (`nxdomain.invalid`) still
gets the wrong errno. #37690 (serve/listen) and #37707 (udp) fix that.
This PR does not touch `bsd.c` or fix #25765.
- Verified: 18 new cases in `test/js/bun/http/serve-listen.test.ts`,
`test/js/bun/net/socket-dns-error.test.ts`,
`test/js/bun/udp/udp_socket.test.ts` and `test/js/bun/udp/dgram.test.ts`
fail on 1.4.1 and pass here. Also ran the serve, socket, net and dgram
suites.

### Background
- `is_valid_hostname` (`src/dns/lib.rs`) accepts an IP literal or an RFC
1035 name (labels of 1 to 63 bytes from the c-ares set, 253 total).
- Node resolves with `dns.lookup` before it binds, so no name reaches a
blocking call.
- Self-reviewed: 2 concerns, both addressed (an unchecked udp `connect`
path; the framing is now the resolver skip).

<details><summary>Notes</summary>

What is and is not shown here:

- The blocking resolver is not observed in this PR. It is the same libc
`getaddrinfo` call, on the same names, that #40970 measured on the
connect side (`dns.lookup` waiting out the macOS resolver timeout for a
label with a space). Linux glibc rejects these names locally, so on
Linux the visible change is only the error.
- No user report exists for a malformed bind hostname. #25765
(`something.localhost` on macOS) is a well-formed name and is #37690's
case.

Repro on 1.4.1 (Linux, no resolver needed for the first four names):

```
serve "this is not a hostname" => ENOENT listen "ENOENT: no such file or directory, listen"
listen "this is not a hostname" => (no code) "Failed to listen at this is not a hostname"
udpSocket "this is not a hostname" => ENOENT bind "bind ENOENT this is not a hostname"
udpSocket connect { hostname: "this is not a hostname" } => DNSException "connect ENOTFOUND this is not a hostname"
dgram socket.connect(1234, "this is not a hostname") via a custom lookup => no error, socket marked connected
serve "aaaa...(64).com" => EMSGSIZE listen
serve "nxdomain.invalid" => EAGAIN listen (left to #37690)
udpSocket "nxdomain.invalid" => ESRCH bind (left to #37707)
```

After this change the first six report `getaddrinfo ENOTFOUND <host>`
(`name: "Error"`, `code: "ENOTFOUND"`, `syscall: "getaddrinfo"`,
`hostname`). `node:net`, `node:http` and `node:dgram` (custom `lookup`)
servers emit the same error object from `listen()` / `bind()`.

Where the check sits:

- `Bun.serve`: at the top of `NewServer::listen`
(`src/runtime/server/mod.rs`), before the uws app is created. The
existing `deinit` path frees the server and `Bun.serve` throws
synchronously, as it does for `EADDRINUSE`. The bracket strip now lives
in one function, `strip_ipv6_brackets`, used by the check and by the
existing `Addr` code. It used to drop the first and last byte of any
hostname that starts with `[`, so `[foo]` resolved as `foo`
(`[localhost]`, `[127.0.0.1]` and `[example.com` were already rejected
by the base URL parse). It now strips only when the inside is an IPv6
literal (`to_ip_address`, so a `%zone` still passes); anything else
keeps its brackets and fails `is_valid_hostname`.
- `Bun.listen` (`src/runtime/socket/Listener.rs`): right after
`SocketConfig::from_js`, only when a port is set (a unix path or an
adopted fd has no hostname). `SocketConfig::from_js` itself is shared
with `Bun.connect`, whose error is delivered asynchronously, so the
check is not placed there.
- `Bun.udpSocket` (`src/runtime/socket/udp_socket.rs`): bind and
`connect.hostname` are both checked right after the config parse, before
the socket is created. Bind failures there throw synchronously today,
and so does this. The `connect` option used to report these names as
`connect ENOTFOUND <host>` (`DNSException`) from the `EAI_*` code; a
well-formed unresolvable `connect.hostname` still does, that block is
#38622's.
- The internal `UDPSocket.jsConnect` that `node:dgram`'s
`socket.connect()` calls gets the same check. `dgram` resolves with
`dns.lookup` first, so only a custom `lookup` can hand it a raw name.
glibc rejected such a name with a code `jsConnect` never read (it checks
for `-1` only), so the socket was marked connected with no error. #38622
reworks that return-code handling; the check here sits before the call
and does not overlap it.

The error is built by `not_a_hostname_error` in
`src/runtime/dns_jsc/cares_jsc.rs` from
`system_error_with_syscall_and_hostname`, the helper `Bun.connect` uses.

For #37690 and #37707: their tests use a 64-byte label, which
`is_valid_hostname` rejects, so once this lands that input is answered
in-process and no longer exercises the `dns_error` plumbing. glibc
rejects `a*b.com`, `a/b.com`, `-a.com` and `ünicode.com` locally
(`EAI_NONAME`, no query sent) while `is_valid_hostname` accepts all
four. Other platforms need checking in CI.

`Bun.listen({ hostname: "[::1]" })` and `Bun.udpSocket({ hostname:
"[::1]" })` never stripped brackets and already fail on 1.4.1 (`Failed
to listen at [::1]`, `bind ENOENT [::1]`). They now fail with
`getaddrinfo ENOTFOUND [::1]`.

Suites run on the debug build: `serve-listen.test.ts` (37 pass),
`socket-dns-error.test.ts` (11 pass), `udp_socket.test.ts` (214 pass),
`dgram.test.ts` + `udp_socket_recv_flags.test.ts` (64 pass),
`serve.test.ts` + `serve-http3.test.ts` + `server-url-invalid.test.ts`
(349 pass, 2 environment failures: root can bind port 1003, egress
proxy), `node-net-server.test.ts`, `serve-epoll-add-fail.test.ts`,
`test/internal/source-lints/` (171 pass), node `test-dgram-bind*`,
`test-dgram-connect*`, `test-dgram-custom-lookup`,
`test-dgram-error-message-address`, `test-net-listen-error`,
`test-http-listening`. `socket.test.ts` and `node-net.test.ts` have the
same `localhost` dual-stack failures on the released bun in this
container.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/js/bun/udp/udp_socket.test.ts, test/js/bun/udp/dgram.test.ts,
test/js/bun/http/serve-listen.test.ts

<!-- robobun:evidence:end -->

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

Development

Successfully merging this pull request may close these issues.

1 participant