Repository navigation
Conversation
…oves `bun ./index.html --port=N` caught EADDRINUSE and retried `port + 1` up to five times in silence, whatever named the port. `--port`, `--host=h:N`, PORT/BUN_PORT/NODE_PORT and the built-in default all walked, and `bun --port=N ./index.html` fell back to 3000 instead. Anything that was told the port out of band then talked to the wrong server. A port named by a flag or an environment variable now stays fixed: the command prints the listen error on one line and exits 1, like Bun.serve(). Under --watch/--hot the process stays up for the next reload, as for any other failing entry point. Only a default port (3000, or `serve.port` in bunfig.toml) moves to the next free one, and it prints `warn: Port N is in use, using port M instead.` to stderr when it does. The TCP listen errors from Bun.serve() gain a `port` own property, like node's listen errors, so the wrapper knows which default was refused without repeating Bun.serve()'s port lookup.
|
Warning Review limit reached
On-demand reviews are free for the next 12 days. After that, they cost $0.25 per reviewed file. Or wait 10 minutes for your next included review. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (4)
Comment |
|
Status: ready for review. CI (build 113083): every lane passes the new tests, including all Windows and macOS lanes. The one red lane is Reproduced on bun 1.4.3 (Linux x64) with the port held by another listener: d=$(mktemp -d); cd $d; printf '<!doctype html><h1>hi</h1>' > index.html
python3 -c "import socket,time;s=socket.socket();s.bind(('127.0.0.1',41321));s.listen(5);time.sleep(30)" &
bun ./index.html --port=41321 --hostname=127.0.0.1 # url: http://127.0.0.1:41322/ (no notice)
PORT=41321 bun ./index.html --hostname=127.0.0.1 # url: http://127.0.0.1:41322/
bun --port=41321 ./index.html --hostname=127.0.0.1 # url: http://127.0.0.1:3000/With this branch the three commands print Tests: |
|
Updated 11:54 AM PT - Sep 8th, 2026
❌ @robobun, your commit df43b9c has 1 failures in 🧪 To try this PR locally: bunx bun-pr 42020That installs a local version of the PR into your bun-42020 --bun |
…ck walk too, keep the lint happy A non-EADDRINUSE listen error on a fallback candidate (EACCES on the next port, for example) was rethrown with a stack trace. Route it to the same one-line error and exit code 1 as the first attempt. Read error.code/error.port once (no-duplicate-conditional-property-access). Test: on Windows the held port sits in the dynamic range, and Windows can hand the child's explicitly bound fallback port to a concurrent port-0 listener in another process as well, so do not assert reachability there. Match the banner URL only as a whole line.
Problem
bun ./index.html --port=Nserves on N+1 when N is in use, with no notice.PORT=Ndoes the same, andbun --port=N ./index.htmlfalls back to 3000.Bun.serve({ port: N })throwsEADDRINUSE. Anything that was told N talks to the wrong server.src/js/internal/html.ts:246. It retriedport + 1five times whatever named the port. Its fallback came from its own argv/env parse, which never sawbun --port.Fix
--port,--host=h:N,bun --port,PORT,BUN_PORTorNODE_PORTstays fixed. OnEADDRINUSEit printserror: Failed to start server. Is port N in use?and exits 1, asnext devdoes.serve.port. It tries the next 10 ports and printswarn: Port N is in use, using port M instead.to stderr.Bun.serve()gain aportown property, like node's listen errors. The wrapper reads it instead of repeating the port lookup.test/js/bun/http/bun-serve-html-entry.test.ts(3 new tests, all fail on stock bun) and one inserve.test.ts. Self-reviewed: 4 concerns, 3 addressed, 1 deferred (Notes).Background
bun ./index.htmlrunssrc/js/internal/html.ts, a wrapper that imports the HTML files asHTMLBundles and callsBun.serve({ static }). Flags after the entry are inprocess.argv, those before it inprocess.execArgv.portundefined,Bun.serve()picks it: the env vars, thenbun --portor bunfigserve.port, else 3000. The wrapper cannot see the bunfig value, henceporton the error.Notes
socket.listen():bun ./index.html --port=41321->url: http://127.0.0.1:41322/, exit 0PORT=41321 bun ./index.html-> 41322, same for--host=127.0.0.1:41321andBUN_PORTbun --port=41321 ./index.html->url: http://127.0.0.1:3000/[serve] port = 41321-> 3000warn: Port 3000 is in use, using port 3001 instead.A taken bunfigserve.portwalks up from that port.--hostname=h:Ncounts as explicit too.src/runtime/server/ServerConfig.rs:647(BUN_PORT,PORT,NODE_PORT, thentransform_options.port, which bothbun --portand bunfigserve.portset). An environment variable pins the port only when its value is the port that was refused, soPORT=abc(whichBun.serve()ignores) still gets the default-port fallback instead of a confusing error about port 3000.serve.portis documented as "the default port forBun.serve", so it is treated as a movable default, not a pinned port.EACCESon a privileged port, for example) now prints its message on one line and exits 1 too, instead of a stack trace intointernal:html. This also holds for a fallback candidate during the walk. Errors that are not listen failures are still thrown.port: 0and so sits in the Windows dynamic range, and Windows can hand the child's explicitly bound fallback port to a concurrentport: 0listener in another process as well (netstatshowed two LISTENING rows for127.0.0.1:<port>from two PIDs on the CI image). A fetch can then reach the other server. Real default ports such as 3000 are outside that range. The warn line and the banner port are still asserted there.bun ./index.html | grep url:).serve.portto a port the test holds, so it exercises the default-port path without touching port 3000 in CI.defaultPort++started at the same number). The new loop starts at N+1 and stops at 65535.--watch/--hota pinned port that is in use leaves the process up for the next reload, as for any failing entry point.process.exit(1)killed--watch/--hotsessions that used to stay alive on a failing entry: addressed, the wrapper now setsprocess.exitCodeand returns. (2)portwas only on theEADDRINUSEbranch: addressed, all three TCP branches carry it. (3) An invalid or late-setPORTmade the wrapper report a port it never asked for: addressed by the value match above. (4) The new stderr lines key their styling onBun.enableANSIColors(stdout or stderr is a TTY), like the rest of this file. html entry, inspector: key banner styling on the fd the banner is written to #41887 adds per-fd flags for built-in JS. Whichever lands second should key these two lines on the stderr flag. Deferred to that sequencing.addresson the listen error (node has it).Bun.serve()may bind several addresses for one hostname, so there is no single obvious value. Left for a follow-up if wanted.localhostbinds that hide a conflict). usockets: stop the listen addrinfo loop on EADDRINUSE so listen(port, "localhost") reports the conflict #36710 is the complementary usockets change. The new tests pass--hostname=127.0.0.1for that reason.--hot, if the process that held a default port goes away and the HTML file is edited, the reload binds the original port and the fallback listener stays open. Stock bun does the same. Out of scope.html.ts'sstart()after its firstawaitis reported twice (visible withbun ./index.html --port=99999).no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/http/serve.test.ts, test/js/bun/http/bun-serve-html-entry.test.ts