Repository navigation
Conversation
A failed listen reached Bun.serve as a bare null socket. on_listen_failed then read thread-local errno, on Linux and Android only, and every other failure became EADDRINUSE with errno 0. uSockets now writes the code of the failing call to *error on every path of a failed listen: socket(), the socket options, bind(), listen(), the poll registration and the unix address. HttpContext::listen forwards it, the uWS listen handler carries it as a second argument, and on_listen_failed decodes it on every platform with SocketGroup::listen_errno, which Bun.listen now uses too.
Collaborator
Author
|
Updated 8:29 AM PT - Oct 7th, 2026
❌ @robobun, your commit 3354ba7 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 44651That installs a local version of the PR into your bun-44651 --bun |
On Windows, bind() can answer WSAEACCES to a socket with SO_EXCLUSIVEADDRUSE when another socket holds the port. Bun.serve sets that option by default, so a second server on one port with no hostname got EACCES once the real code reached it.
Name the arguments of uws_listen_handler in its type. Split SocketGroup::listen_errno by platform so each arm shows what it decodes. Keep the contract of *error in libusockets.h only.
The uWS listen functions return the listen socket and take the out-params of us_socket_group_listen: *error, and a new *dns_error for the result of a failed getaddrinfo(). The listen callback and its trampolines are gone. Bun.serve and Bun.listen decode both codes the same way. A hostname that does not resolve throws `getaddrinfo ENOTFOUND <host>`. Bun.listen sets the negative libuv errno. Bun.serve reports WSAEACCES as EADDRINUSE in its own error path, and uSockets keeps the code that Winsock gave.
This was referenced Oct 7, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #30363
Problem
Bun.serve()orhttp.Server#listen()throwsFailed to start server. Is port N in use?withcode: "EADDRINUSE",errno: 0. On Linuxerrnois 0 for EADDRINUSE and EACCES.HttpContext::listen(packages/bun-uws/src/HttpContext.h:1286) drops theint *errorof uSockets.on_listen_failed(src/runtime/server/mod.rs:2046) reads thread-local errno, on Linux only.socket(),setsockopt()orgetaddrinfo()fails.Bun.listenthen has nocode.Fix
*error, and a failedgetaddrinfo()result to a new*dns_error. uWS returns the listen socket and both codes. The listen callback is gone.Bun.serveandBun.listendecode both. A failed lookup throwsgetaddrinfo ENOTFOUND <host>.Bun.listensets Node's negativeerrno.test/js/bun/http/serve-listen.test.tsand five more files, with the other suites in Notes.Background
Downsides
Bun.listenerrors have a negativeerrno(was positive).Bun.servereportsWSAEACCESas EADDRINUSE. A port in a reserved range still readsIs port N in use?.intout-params, no new allocation or system call.Notes
What JS sees on Linux x64.
mainis the release build1.4.3-canary.1+367d939d9. Its listen error code is the same as the base of this PR. The PR column is a debug ASAN build.mainBun.serveandhttp.ServerEADDRINUSE,errno: 0EADDRINUSE,-98EADDRINUSE,-98Bun.listenandnet.Servererrno: 98-98-98127.0.0.1:80as a user that may not bind it (the report)EACCES,errno: 0EACCES,-13EACCES,-13192.0.2.1,Bun.serveEADDRNOTAVAIL,-99EADDRNOTAVAIL,-99getaddrinfo()answersEAI_NONAMEBun.serve: a stale errno (ENOENT ... listen).Bun.listen: nocodegetaddrinfo ENOTFOUND <host>getaddrinfo ENOTFOUND <host>EAI_AGAINgetaddrinfo EAI_AGAIN <host>fe80::1%nope0Bun.serve:EINVAL.Bun.listen: nocodeEINVAL,-22EINVAL,-22127.0.0.1orlocalhostat the descriptor limitBun.serve:EMFILE.Bun.listen: nocodeEMFILE,-24setsockopt()fails with EPERMBun.serve:EPERM.Bun.listen: nocodeEPERM,-1On
main,Bun.servegets the rows below the report right on Linux only, from thread-local errno. macOS and Windows read no errno there.Refs #25765:
hostname: "something.localhost"printedIs port 3000 in use?on macOS. It now throwsgetaddrinfo ENOTFOUND something.localhost. The server still does not start.The error of a failed
getaddrinfo()*dns_error, andc_ares::Error::init_eaidecodes it as it does forBun.connect.EAI_NONAMEfor its first lookup andEAI_SYSTEMafter that. Both leave EMFILE in errno, and uSockets reports that errno in*error.Windows. I have no Windows machine. CI is the only check.
*errorholds a WSA or Win32 code.SocketGroup::listen_errnodecodes it through the Win32 table.Bun.listenused the errno form, which readsERROR_PATH_NOT_FOUND(3) as ESRCH.SetLastError. They now go to*error: a path that is too long, and a missing directory.WSAEACCESwhen another socket holds the port in some modes: in build 123622 a secondBun.serveon one port with no hostname got EACCES.on_listen_failedreports EACCES as EADDRINUSE on Windows.Bun.listenkeeps the code that Winsock gave.init_eaitakes libuv numbers there andgetaddrinfo()returns WSA codes.Tests. These fail without the fix on Linux:
serve-listen.test.ts: the eight busy port cases (http and https) anda privileged port throws EACCES, which is the command of the report.serve-syscall-fault.test.ts:poll_start → Bun.serve throws the error of the failed registration(ASAN builds only).serve-epoll-add-fail.test.ts: theBun.listenerrno, the twosetsockopt()cases and thegetaddrinfo()case. They use theLD_PRELOADshim of that file.tcp-server.test.ts:Bun.listen at the file descriptor limit throws EMFILEandBun.listen reports why a unix path cannot be bound.node-http.test.tsandnode-net-server.test.ts:a listen() error carries the errno of the failed bind. Both pass in Node.The other new cases pass before and after on Linux, because
mainreads errno there. They guard macOS and Windows, and the Linux results that must not change.Suites run with the fix (Linux x64, debug ASAN build): the six files above,
fetch.unix.test.ts,socket-syscall-fault.test.ts,socket-dns-error.test.ts,unix-socket-unlink.test.ts,unix-socket-long-path.test.ts,socket.test.ts,node-net.test.ts,serve.test.ts, and the node teststest-http-bind-twice,test-net-bind-twice,test-net-server-listen-path,test-net-server-try-ports,test-net-eaddrinuse,test-net-listen-error,test-cluster-eaddrinuse,test-cluster-shared-handle-bind-error,test-cluster-eaccess.bun run rust:check-allpasses.In my container the debug build runs some subprocess tests past their 5 s limit: the
a dispatch that throwsgroup ofnode-http.test.ts, several tests ofnode-net.test.tsandsocket.test.ts, and the half-open test ofnode-net-server.test.ts. Their fixtures print the expected result when I run them by hand, and the half-open test times out the same way on a build of the previous head. One test ofserve.test.ts(#7187) expects EACCES as root and fails with and without this change.Self-review: the 5 concerns that are partly done
socket()fail. The descriptor limit tests needulimit.Not in this PR
Bun.serve: a failed UDP listen throws anErrorwith nocode.dgrambind still reports a positiveerrno.tls.cathrowsFailed to create HTTP serverwith nocode.addressandporton theBun.serveerror. fix(node): format HTTP listen errors #42742 adds them innode:http.bind(), the socket file stays on disk.Open PRs on the same code
dns_error) and both of its sinks. It can close.errortobsd_create_socketat the same three lines, with the same text.listen_fdwith the same return shape. The one that merges second changes oneon_listencall.bsd_create_listen_socket.node:httplisten errors fromerr.code.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/node/net/node-net-server.test.ts, test/js/node/http/node-http.test.ts, test/js/bun/net/tcp-server.test.ts, test/js/bun/http/serve-syscall-fault.test.ts, test/js/bun/http/serve-listen.test.ts, test/js/bun/http/serve-epoll-add-fail.test.ts