From 6e2cc0ceb2211a4e8941650bd51d9517c39e84e9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:51:01 +0000 Subject: [PATCH] Report an unresolvable listen hostname as a getaddrinfo error 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 ". 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 ` 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. --- packages/bun-usockets/src/bsd.c | 6 +- packages/bun-usockets/src/context.c | 4 +- .../src/internal/networking/bsd.h | 3 +- packages/bun-usockets/src/libusockets.h | 13 ++- packages/bun-uws/src/App.h | 36 ++++----- packages/bun-uws/src/HttpContext.h | 4 +- src/runtime/server/mod.rs | 35 +++++--- src/runtime/socket/Listener.rs | 12 +++ src/uws_sys/App.rs | 6 +- src/uws_sys/SocketGroup.rs | 4 + src/uws_sys/_libusockets.h | 3 +- src/uws_sys/libuwsockets.cpp | 16 ++-- test/js/bun/http/serve-listen.test.ts | 80 +++++++++++++++++++ test/js/bun/net/socket-dns-error.test.ts | 37 +++++++-- test/js/node/net/node-net-server.test.ts | 46 ++++++++++- 15 files changed, 250 insertions(+), 55 deletions(-) diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index bee84866b337..0e35527be4a3 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -1216,7 +1216,7 @@ int bsd_set_defer_accept(LIBUS_SOCKET_DESCRIPTOR listenFd) { // return LIBUS_SOCKET_ERROR or the fd that represents listen socket // listen both on ipv6 and ipv4 -LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error) { +LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error, int* dns_error) { struct addrinfo hints, *result; memset(&hints, 0, sizeof(struct addrinfo)); @@ -1227,7 +1227,9 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int char port_string[16]; snprintf(port_string, 16, "%d", port); - if (getaddrinfo(host, port_string, &hints, &result)) { + int gai_error = getaddrinfo(host, port_string, &hints, &result); + if (gai_error != 0) { + *dns_error = gai_error; return LIBUS_SOCKET_ERROR; } diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index 68b4216266b3..69c6df4ea31e 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -388,8 +388,8 @@ static void us_internal_init_listen_socket(struct us_listen_socket_t *ls, struct us_listen_socket_t *us_socket_group_listen(struct us_socket_group_t *group, unsigned char kind, struct ssl_ctx_st *ssl_ctx, - const char *host, int port, int options, int socket_ext_size, int *error) { - LIBUS_SOCKET_DESCRIPTOR listen_socket_fd = bsd_create_listen_socket(host, port, options, error); + const char *host, int port, int options, int socket_ext_size, int *error, int *dns_error) { + LIBUS_SOCKET_DESCRIPTOR listen_socket_fd = bsd_create_listen_socket(host, port, options, error, dns_error); if (listen_socket_fd == LIBUS_SOCKET_ERROR) { return 0; } diff --git a/packages/bun-usockets/src/internal/networking/bsd.h b/packages/bun-usockets/src/internal/networking/bsd.h index 239d0979d754..451bbbedf75c 100644 --- a/packages/bun-usockets/src/internal/networking/bsd.h +++ b/packages/bun-usockets/src/internal/networking/bsd.h @@ -230,7 +230,8 @@ int bsd_send_is_transient_error(); // return LIBUS_SOCKET_ERROR or the fd that represents listen socket // listen both on ipv6 and ipv4 -LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error); +// error / dns_error: see us_socket_group_listen() in libusockets.h +LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error, int* dns_error); LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket_unix(const char *path, size_t pathlen, int options, int* error); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index cf62e75eee44..2836e81cc0c5 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -386,11 +386,18 @@ void us_socket_start_tls_handshake(us_socket_r s) nonnull_fn_decl; /* ── Listen ─────────────────────────────────────────────────────────────── * The listener owns: an embedded group for accepted sockets, the SSL_CTX * (borrowed ref, optional), the SNI tree (optional), and the kind to stamp on - * accepted sockets. */ + * accepted sockets. + * + * The out-params are only meaningful when NULL is returned. A `host` that did + * not resolve leaves the raw getaddrinfo(3) return code in *dns_error and does + * not touch *error; failures after resolution report errno (WSAGetLastError() + * on Windows) through *error. The two number spaces overlap, so they are kept + * apart the same way us_connecting_socket_t tags error_is_dns. Callers zero + * both before the call. */ struct us_listen_socket_t *us_socket_group_listen(us_socket_group_r group, unsigned char kind, struct ssl_ctx_st *ssl_ctx, - const char *host, int port, int options, int socket_ext_size, int *error) - __attribute__((nonnull(1, 8))); /* ssl_ctx, host nullable */ + const char *host, int port, int options, int socket_ext_size, int *error, int *dns_error) + __attribute__((nonnull(1, 8, 9))); /* ssl_ctx, host nullable */ struct us_listen_socket_t *us_socket_group_listen_unix(us_socket_group_r group, unsigned char kind, struct ssl_ctx_st *ssl_ctx, const char *path, size_t pathlen, int options, int socket_ext_size, int *error) diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index eb5ee2ba132d..d2f92c089856 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -713,35 +713,35 @@ struct TemplatedApp { struct ssl_ctx_st *sslCtxOrNull() { return SSL ? sslCtx : nullptr; } + /* (listen socket or nullptr, dns_error as documented on us_socket_group_listen) */ + using ListenHandler = MoveOnlyFunction; + + TemplatedApp &&listenTcp(const char *host, int port, int options, ListenHandler &&handler) { + int dnsError = 0; + us_listen_socket_t *listenSocket = httpContext ? trackListenSocket(httpContext->listen(sslCtxOrNull(), host, port, options, &dnsError)) : nullptr; + handler(listenSocket, dnsError); + return std::move(*this); + } + public: /* Host, port, callback */ - TemplatedApp &&listen(const std::string &host, int port, MoveOnlyFunction &&handler) { - if (host.empty()) { - return listen(port, std::move(handler)); - } - handler(httpContext ? trackListenSocket(httpContext->listen(sslCtxOrNull(), host.c_str(), port, 0)) : nullptr); - return std::move(*this); + TemplatedApp &&listen(const std::string &host, int port, ListenHandler &&handler) { + return listenTcp(host.empty() ? nullptr : host.c_str(), port, 0, std::move(handler)); } /* Host, port, options, callback */ - TemplatedApp &&listen(const std::string &host, int port, int options, MoveOnlyFunction &&handler) { - if (host.empty()) { - return listen(port, options, std::move(handler)); - } - handler(httpContext ? trackListenSocket(httpContext->listen(sslCtxOrNull(), host.c_str(), port, options)) : nullptr); - return std::move(*this); + TemplatedApp &&listen(const std::string &host, int port, int options, ListenHandler &&handler) { + return listenTcp(host.empty() ? nullptr : host.c_str(), port, options, std::move(handler)); } /* Port, callback */ - TemplatedApp &&listen(int port, MoveOnlyFunction &&handler) { - handler(httpContext ? trackListenSocket(httpContext->listen(sslCtxOrNull(), nullptr, port, 0)) : nullptr); - return std::move(*this); + TemplatedApp &&listen(int port, ListenHandler &&handler) { + return listenTcp(nullptr, port, 0, std::move(handler)); } /* Port, options, callback */ - TemplatedApp &&listen(int port, int options, MoveOnlyFunction &&handler) { - handler(httpContext ? trackListenSocket(httpContext->listen(sslCtxOrNull(), nullptr, port, options)) : nullptr); - return std::move(*this); + TemplatedApp &&listen(int port, int options, ListenHandler &&handler) { + return listenTcp(nullptr, port, options, std::move(handler)); } /* options, callback, path to unix domain socket */ diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 6f7cf7441cca..ad57ce81ae65 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -1047,11 +1047,11 @@ struct HttpContext { } /* Listen to port using this HttpContext. ssl_ctx may be nullptr for plain HTTP. */ - us_listen_socket_t *listen(struct ssl_ctx_st *sslCtx, const char *host, int port, int options) { + us_listen_socket_t *listen(struct ssl_ctx_st *sslCtx, const char *host, int port, int options, int *dnsError) { int error = 0; /* HTTP clients always send first (the request, or ClientHello for TLS), so defer * accept() until data arrives and dispatch the read immediately after accept. */ - auto socket = us_socket_group_listen(&group, socketKind(), sslCtx, host, port, options | LIBUS_LISTEN_DEFER_ACCEPT, socketExtSize(), &error); + auto socket = us_socket_group_listen(&group, socketKind(), sslCtx, host, port, options | LIBUS_LISTEN_DEFER_ACCEPT, socketExtSize(), &error, dnsError); // we dont depend on libuv ref for keeping it alive if (socket) { us_socket_unref(&socket->s); diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 564d7dc4c855..447fd964bf9f 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -27,6 +27,7 @@ fn server_js_create( } } +use bun_cares_sys::c_ares_draft as c_ares; use bun_io::KeepAlive; use bun_uws as uws; use bun_uws_sys as uws_sys; @@ -1961,9 +1962,13 @@ impl NewServer { } } - pub(crate) fn on_listen(&mut self, socket: Option<*mut uws_sys::app::ListenSocket>) { + pub(crate) fn on_listen( + &mut self, + socket: Option<*mut uws_sys::app::ListenSocket>, + dns_error: c_int, + ) { let Some(socket) = socket else { - return self.on_listen_failed(); + return self.on_listen_failed(dns_error); }; self.listener = Some(socket); // SAFETY: `vm_mut()` is the process-static `*mut VirtualMachine` (non-null @@ -1983,22 +1988,30 @@ impl NewServer { /// error-stack drain is still TODO; the EADDRINUSE/ /// EACCES paths below cover the node:http `server.listen` error contract. #[cold] - pub(crate) fn on_listen_failed(&mut self) { + pub(crate) fn on_listen_failed(&mut self, dns_error: c_int) { self.listener = None; let global = self.global_this(); let error_instance = match &self.config.address { - server_config::Address::Tcp { - port, - hostname: _hostname, - } => { + server_config::Address::Tcp { port, hostname } => { + // The hostname did not resolve: no socket call ran, so errno below is meaningless. + if let Some(dns_err) = c_ares::Error::init_eai(dns_error) { + let host = hostname.as_ref().map(|h| h.as_bytes()).unwrap_or(b""); + let err = crate::dns_jsc::cares_jsc::system_error_with_syscall_and_hostname( + dns_err, + b"getaddrinfo", + host, + ); + let _ = global.throw_value(err.to_error_instance(global)); + return; + } // Rust's `target_os = "linux"` excludes // Android, so match both explicitly. #[cfg(any(target_os = "linux", target_os = "android"))] { let errno = bun_sys::get_errno(-1i32); if errno == bun_sys::E::EACCES { - let host = _hostname + let host = hostname .as_ref() .map(|h| h.as_bytes()) .unwrap_or(b"0.0.0.0"); @@ -3298,6 +3311,7 @@ mod trampoline { pub(super) extern "C" fn on_listen( socket: *mut UwsListenSocket, + dns_error: c_int, user_data: *mut c_void, ) { // SAFETY: user_data is the `*mut NewServer<..>` passed to listen_with_config. @@ -3307,7 +3321,7 @@ mod trampoline { } else { Some(socket.cast::>()) }; - server.on_listen(socket); + server.on_listen(socket, dns_error); } pub(super) extern "C" fn on_listen_unix( @@ -3316,7 +3330,8 @@ mod trampoline { _flags: i32, user_data: *mut c_void, ) { - on_listen::(socket, user_data); + // A unix path is never resolved, so there is no dns_error to forward. + on_listen::(socket, 0, user_data); } pub(super) extern "C" fn on_404( diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index d1c3a18fe91a..99879dda44c8 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -7,6 +7,7 @@ use core::ptr::NonNull; use std::rc::Rc; use bun_boringssl_sys as boring_sys; +use bun_cares_sys::c_ares_draft as c_ares; use bun_io::KeepAlive; use bun_jsc::ZigStringJsc as _; use bun_jsc::strong::Optional as Strong; @@ -439,6 +440,7 @@ impl Listener { .map(|p| p.as_ptr().cast::()); let mut errno: c_int = 0; + let mut dns_error: c_int = 0; let listen_socket: *mut uws_sys::ListenSocket = match &mut connection { UnixOrHost::Host { host, port } => { let hostz = bun_core::ZBox::from_bytes(&host[..]); @@ -452,6 +454,7 @@ impl Listener { socket_flags, size_of::<*mut c_void>() as c_int, &mut errno, + &mut dns_error, ) }); if !ls.is_null() { @@ -494,6 +497,15 @@ impl Listener { UnixOrHost::Unix(u) => u, UnixOrHost::Fd(_) => b"", }; + if let Some(dns_err) = c_ares::Error::init_eai(dns_error) { + log!("Failed to resolve listen hostname {}", dns_error); + let err = crate::dns_jsc::cares_jsc::system_error_with_syscall_and_hostname( + dns_err, + b"getaddrinfo", + hostname_bytes, + ); + return Err(global.throw_value(err.to_error_instance(global))); + } let err = global.create_error_instance(format_args!( "Failed to listen at {}", bstr::BStr::new(hostname_bytes) diff --git a/src/uws_sys/App.rs b/src/uws_sys/App.rs index c051b5aeb846..31c7b646dd38 100644 --- a/src/uws_sys/App.rs +++ b/src/uws_sys/App.rs @@ -244,7 +244,7 @@ impl App { pub fn listen( &mut self, port: i32, - handler: extern "C" fn(*mut UwsListenSocket, *mut c_void), + handler: extern "C" fn(*mut UwsListenSocket, c_int, *mut c_void), user_data: *mut c_void, ) { // Callers supply the C-ABI shim directly (see the RouteHandler note above). @@ -462,7 +462,9 @@ pub(crate) type uws_app_t = uws_app_s; pub mod c { use super::*; - pub(crate) type uws_listen_handler = Option; + /// `(listen_socket, dns_error, user_data)`; `dns_error` as in `SocketGroup::listen`. + pub(crate) type uws_listen_handler = + Option; pub(crate) type uws_method_handler = Option; // The C++ shim hands the filter the uws_res_t*, which for HTTP server diff --git a/src/uws_sys/SocketGroup.rs b/src/uws_sys/SocketGroup.rs index 2dfe607e99a1..6f1bb9d26366 100644 --- a/src/uws_sys/SocketGroup.rs +++ b/src/uws_sys/SocketGroup.rs @@ -153,6 +153,7 @@ impl SocketGroup { && self.low_prio_count == 0 } + /// On null: `dns_err` (getaddrinfo code) is set if `host` did not resolve, else `err` (errno). pub fn listen( &mut self, kind: SocketKind, @@ -162,6 +163,7 @@ impl SocketGroup { options: c_int, socket_ext_size: c_int, err: &mut c_int, + dns_err: &mut c_int, ) -> *mut ListenSocket { // SAFETY: forwarding to C; all pointers are valid or null as documented. unsafe { @@ -174,6 +176,7 @@ impl SocketGroup { options, socket_ext_size, err, + dns_err, ) } } @@ -318,6 +321,7 @@ unsafe extern "C" { options: c_int, socket_ext_size: c_int, err: *mut c_int, + dns_err: *mut c_int, ) -> *mut ListenSocket; fn us_socket_group_listen_unix( group: *mut SocketGroup, diff --git a/src/uws_sys/_libusockets.h b/src/uws_sys/_libusockets.h index 8f49e123ca36..57a26fdae83a 100644 --- a/src/uws_sys/_libusockets.h +++ b/src/uws_sys/_libusockets.h @@ -123,8 +123,9 @@ typedef struct { uws_websocket_close_handler close; } uws_socket_behavior_t; +/* dns_error: getaddrinfo(3) return code if the host did not resolve, else 0. */ typedef void (*uws_listen_handler)(struct us_listen_socket_t* listen_socket, - void* user_data); + int dns_error, void* user_data); typedef void (*uws_listen_domain_handler)( struct us_listen_socket_t* listen_socket, const char* domain, int options, void* user_data); diff --git a/src/uws_sys/libuwsockets.cpp b/src/uws_sys/libuwsockets.cpp index 89a62dcca2b1..aa4d0e01236c 100644 --- a/src/uws_sys/libuwsockets.cpp +++ b/src/uws_sys/libuwsockets.cpp @@ -439,16 +439,16 @@ extern "C" { uWS::SSLApp *uwsApp = (uWS::SSLApp *)app; uwsApp->listen(port, [handler, - user_data](struct us_listen_socket_t *listen_socket) - { handler((struct us_listen_socket_t *)listen_socket, user_data); }); + user_data](struct us_listen_socket_t *listen_socket, int dns_error) + { handler(listen_socket, dns_error, user_data); }); } else { uWS::App *uwsApp = (uWS::App *)app; uwsApp->listen(port, [handler, - user_data](struct us_listen_socket_t *listen_socket) - { handler((struct us_listen_socket_t *)listen_socket, user_data); }); + user_data](struct us_listen_socket_t *listen_socket, int dns_error) + { handler(listen_socket, dns_error, user_data); }); } } @@ -462,9 +462,9 @@ extern "C" uWS::SSLApp *uwsApp = (uWS::SSLApp *)app; uwsApp->listen( hostname, port, options, - [handler, user_data](struct us_listen_socket_t *listen_socket) + [handler, user_data](struct us_listen_socket_t *listen_socket, int dns_error) { - handler((struct us_listen_socket_t *)listen_socket, user_data); + handler(listen_socket, dns_error, user_data); }); } else @@ -472,9 +472,9 @@ extern "C" uWS::App *uwsApp = (uWS::App *)app; uwsApp->listen( hostname, port, options, - [handler, user_data](struct us_listen_socket_t *listen_socket) + [handler, user_data](struct us_listen_socket_t *listen_socket, int dns_error) { - handler((struct us_listen_socket_t *)listen_socket, user_data); + handler(listen_socket, dns_error, user_data); }); } } diff --git a/test/js/bun/http/serve-listen.test.ts b/test/js/bun/http/serve-listen.test.ts index e7a9582414c0..7f3fb5b793e9 100644 --- a/test/js/bun/http/serve-listen.test.ts +++ b/test/js/bun/http/serve-listen.test.ts @@ -161,6 +161,86 @@ describe.each([ }); }); +describe("Bun.serve() with a hostname that does not resolve", () => { + // A DNS label longer than 63 bytes is invalid (RFC 1035 section 2.3.4), so + // getaddrinfo rejects these locally without touching the network. + const unresolvable = Buffer.alloc(64, "a").toString() + ".com"; + + function resolverError(hostname: string) { + return { + name: "Error", + code: "ENOTFOUND", + syscall: "getaddrinfo", + hostname, + message: `getaddrinfo ENOTFOUND ${hostname}`, + }; + } + + function listenError(options: Parameters[0]) { + let server; + try { + server = serve(options); + } catch (error: any) { + const { name, code, syscall, hostname, message } = error; + return { name, code, syscall, hostname, message }; + } + server.stop(true); + throw new Error(`expected Bun.serve() to throw, but it is listening on ${server.url}`); + } + + test.each([ + ["http", {}], + ["https", { tls }], + ])("%s: throws the resolver error instead of a listen error", (_, extra) => { + expect( + listenError({ + ...extra, + hostname: unresolvable, + port: 0, + fetch() { + return new Response(); + }, + }), + ).toEqual(resolverError(unresolvable)); + }); + + test("the error does not depend on what the resolver left behind in errno", () => { + // These used to surface as whatever errno getaddrinfo happened to leave + // behind (EAGAIN / ENOENT / EMSGSIZE, varying with the input) attributed + // to listen(2). The error must describe the lookup of the hostname as + // given, including the brackets Bun strips before resolving. + const hostnames = [unresolvable, `[${unresolvable}]`, Buffer.alloc(1100, "a").toString() + ".com"]; + expect( + hostnames.map(hostname => + listenError({ + hostname, + port: 0, + fetch() { + return new Response(); + }, + }), + ), + ).toEqual(hostnames.map(resolverError)); + }); + + test("a later Bun.serve() still works", () => { + listenError({ + hostname: unresolvable, + port: 0, + fetch() { + return new Response(); + }, + }); + using server = serve({ + port: 0, + fetch() { + return new Response(); + }, + }); + expect(server.port).toBeGreaterThan(0); + }); +}); + // Linux-only: uses /proc/self/fd to find the listen socket and close it from // under the server so getsockname() fails with EBADF. test.skipIf(!isLinux)("server.address / server.port do not panic when getsockname() fails", async () => { diff --git a/test/js/bun/net/socket-dns-error.test.ts b/test/js/bun/net/socket-dns-error.test.ts index 6db4c3eda121..ff76cc2415ca 100644 --- a/test/js/bun/net/socket-dns-error.test.ts +++ b/test/js/bun/net/socket-dns-error.test.ts @@ -1,10 +1,11 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, tls } from "harness"; -// `Bun.connect` to a hostname that fails to resolve must surface the resolver -// error (code `ENOTFOUND`, `syscall: "getaddrinfo"`, `hostname`), matching -// `node:dns`, rather than collapsing it into `ECONNREFUSED` / `syscall: -// "connect"` as if a listener had refused the connection. +// `Bun.connect` / `Bun.listen` with a hostname that fails to resolve must +// surface the resolver error (code `ENOTFOUND`, `syscall: "getaddrinfo"`, +// `hostname`), matching `node:dns`, rather than collapsing it into +// `ECONNREFUSED` / `syscall: "connect"` as if a listener had refused the +// connection, or (for listen) a code-less "Failed to listen" error. // // These live in their own file because `socket.test.ts` is a large // `describe.concurrent` block whose dual-stack `localhost` tests are @@ -119,3 +120,29 @@ test("consecutive Bun.connect calls to the same unresolvable hostname all get th } expect(errors).toEqual([EXPECTED, EXPECTED, EXPECTED]); }); + +test.each([ + ["tcp", {}], + ["tls", { tls }], +])("Bun.listen (%s) on an unresolvable hostname throws the resolver error", (_, extra) => { + // Three attempts: each failed listen tears down a half-built listener, so a + // second and third attempt must neither crash nor report something else. + const errors = []; + for (let i = 0; i < 3; i++) { + let listener; + try { + listener = Bun.listen({ + ...extra, + hostname: UNRESOLVABLE_HOST, + port: 0, + socket: { open() {}, data() {} }, + }); + } catch (e) { + errors.push(pick(e)); + continue; + } + listener.stop(true); + throw new Error(`expected Bun.listen to throw, but it is listening on port ${listener.port}`); + } + expect(errors).toEqual([EXPECTED, EXPECTED, EXPECTED]); +}); diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 2fb444c2e07e..3c00c1e5a3db 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -1,10 +1,13 @@ import { realpathSync } from "fs"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, tls as certs, tempDir } from "harness"; +import { createServer as createHttpServer } from "http"; +import { createServer as createHttpsServer } from "https"; import { AddressInfo, createServer, Server, Socket } from "net"; import { createTest } from "node-harness"; import { once } from "node:events"; import { tmpdir } from "os"; import { join } from "path"; +import { createServer as createTlsServer } from "tls"; const { describe, expect, it, createCallCheckCtx } = createTest(import.meta.path); @@ -229,6 +232,47 @@ describe("net.createServer listen", () => { }); }); +describe("server.listen() on a hostname that does not resolve", () => { + // A DNS label longer than 63 bytes is invalid (RFC 1035 section 2.3.4), so + // getaddrinfo rejects it locally without touching the network. + const unresolvable = Buffer.alloc(64, "a").toString() + ".com"; + + // Node emits the dns.lookup() failure as-is: it names the hostname and + // getaddrinfo, not a listen(2) errno. + const expected = { + name: "Error", + code: "ENOTFOUND", + syscall: "getaddrinfo", + hostname: unresolvable, + message: `getaddrinfo ENOTFOUND ${unresolvable}`, + }; + + function listenError(server: Server) { + const { promise, resolve, reject } = Promise.withResolvers>(); + server.once("error", ({ name, code, syscall, hostname, message }: any) => + resolve({ name, code, syscall, hostname, message }), + ); + server.once("listening", () => { + const address = server.address(); + server.close(); + reject(new Error(`expected listen() to fail, but the server is listening on ${JSON.stringify(address)}`)); + }); + server.listen(0, unresolvable); + return promise; + } + + // net/tls servers listen through Bun.listen, http/https servers through + // Bun.serve: two native paths that must report the same error. + it.each([ + ["net", () => createServer()], + ["tls", () => createTlsServer(certs)], + ["http", () => createHttpServer()], + ["https", () => createHttpsServer(certs)], + ])("%s server emits the resolver error", async (_, create) => { + expect(await listenError(create())).toEqual(expected); + }); +}); + describe("net.createServer events", () => { it("should receive data", done => { const { mustCall, mustNotCall } = createCallCheckCtx(done);