Skip to content

Bun.serve: throw when both port and unix are given - #41660

Open
robobun wants to merge 3 commits into
mainfrom
robobun/65fbe36a/serve-unix-port-conflict
Open

robobun wants to merge 3 commits into
mainfrom
robobun/65fbe36a/serve-unix-port-conflict

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.serve({ port: 8080, unix: "/tmp/app.sock", fetch }) binds only the unix socket. The port is dropped with no diagnostic: server.port is undefined and nothing listens on 8080.
  • The cause is src/runtime/server/ServerConfig.rs:1115. The unix branch overwrites args.address after port was parsed into it. The sibling conflict, unix with hostname, already throws Cannot specify both hostname and unix a few lines above.

Fix

  • Throw Cannot specify both port and unix when unix is given with a non-zero port. Port 0 asks for no specific port, so it keeps its current behavior (the unix socket wins). In-tree tests rely on port: 0 with unix.
  • node:http's Server.listen forwarded port, host and unix: socketPath together to Bun.serve. It now follows Node's net.Server.listen: when the options carry a port key (a number, a string, undefined or null), the port wins and the path is not forwarded. When the path wins, host is not forwarded (Node's pipe branch never reads it, and listen({ path, host }) emitted 'error' in bun before). node:net already applies the port rule in src/js/node/net.ts. Verified against node v26.3.0.
  • Correct because the public types already declare unix exclusive with hostname and port (packages/bun-types/serve.d.ts, XOR<HostnamePortServeOptions, UnixServeOptions>). The runtime now enforces that contract.
  • Verified: test/js/bun/http/serve.test.ts (new test) and test/js/node/http/node-http.test.ts (five new cases). All fail on bun 1.4.3 and pass with the fix, on linux x64 and Windows x64. Also ran bun-serve-args.test.ts and both full files.

Background

  • ServerConfig::from_js parses the Bun.serve options object into an Address, which is either Tcp { hostname, port } or Unix(path). Each option is read in sequence, so a later option can silently replace an earlier one.
  • node:http in bun is built on Bun.serve. Server.prototype.listen in src/js/node/_http_server.ts normalizes the Node argument shapes and calls Bun.serve in kRealListen.
  • In Node, Server.listen(options) checks options.port before options.path. A port key of any accepted type takes the TCP branch and path is never looked at. The pipe branch reads only path and the pipe-specific options.
Notes

Found by an internal audit of places where bun picks a transport mode from conflicting input.

The first revision of the Bun.serve test asserted on server.url. Its getter throws TypeError: Invalid URL for a Windows drive path (unix://C:\...), which is the pre-existing #40182 (fix proposed in #35806). The test now asserts server.port is undefined and fetches over the unix socket instead.

Two pre-existing failures in serve.test.ts in my linux container (#7187 root range port, /bun:info loopback clients) also fail on main there. They depend on the container running as root and on its network interfaces. They are unrelated to this change and pass in CI.

Self-reviewed: 4 concerns raised, 4 addressed (match the net.ts port precedence rule including string ports and Node's undefined/null normalization, keep the port: 0 + unix boundary asserted next to the throw test, use tempDir in the new test, do not cite #14299 as precedent). Review follow-ups addressed: listen({ path, host }) parity, Windows-portable test assertion.


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

Bun.serve({ port, unix }) bound only the unix socket and dropped the
port without a diagnostic. The sibling conflict, unix with hostname,
already throws. Throw "Cannot specify both port and unix" for a
non-zero port. Port 0 asks for no specific port and keeps its current
behavior.

node:http's listen({ port, path }) now binds the port and ignores the
path, as node does, so it does not reach the new check.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 14 days. After that, they cost $0.25 per reviewed file.

Or wait 4 minutes for your next included review.

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

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 9ecfc0f5-2b69-4cce-add2-341555068adf

📥 Commits

Reviewing files that changed from the base of the PR and between d316760 and fec129a.

📒 Files selected for processing (4)
  • src/js/node/_http_server.ts
  • src/runtime/server/ServerConfig.rs
  • test/js/bun/http/serve.test.ts
  • test/js/node/http/node-http.test.ts

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

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:10 PM PT - Sep 6th, 2026

❌ @robobun, your commit fec129a has 2 failures in Build #111887 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41660

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

bun-41660 --bun

Comment thread test/js/bun/http/serve.test.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
…t portable

listen({ path, host }) forwarded both to Bun.serve as hostname + unix,
which throws. Node's pipe branch never reads host, so drop it there.

The Bun.serve test asserted on server.url, whose getter throws for a
Windows drive path (pre-existing, see #35806). Assert reachability over
the unix socket instead.
Comment thread src/js/node/_http_server.ts Outdated

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines +586 to +590
if ((typeof port === "number" || typeof port === "string") && socketPath) {
socketPath = undefined;
} else if (socketPath) {
host = undefined;
}

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) When path wins the precedence check, port is not cleared, so a truthy non-number/non-string port (e.g. true, [8080], new Number(80)) is still forwarded to Bun.serve alongside unix, where the new ServerConfig guard now throws — listen({ port: true, path }) bound the socket on base but emits 'error' after this PR, and Node binds the pipe. Fix: set port = undefined in the else if (socketPath) branch so the pipe path never forwards a port, matching src/js/node/net.ts:3745.

Extended reasoning...

listen({ port: true, path: sock }): line 548 sets port = true; line 552 skips (not undefined/null); line 578 skips (not string); line 586 typeof true is "boolean" so the first branch is false; line 588 takes else if (socketPath) and clears host but leaves port = true and socketPath = sock. kRealListen then calls Bun.serve({ port: true, unix: sock, ... }). In ServerConfig::from_js, get_truthy("port") returns Some(true), to_number(true) is 1, has_port = true (line 1087); the unix block then hits the new has_port guard and throws "Cannot specify both port and unix" (line 1125-1129). The throw is caught at line 627 and routed to emitListenErrorNextTick, so the server emits 'error' instead of 'listening'. On the base branch the same inputs reached Bun.serve, port was parsed into args.address and then silently overwritten by unix, so the socket bound. Node's net.Server.listen only takes the TCP branch when typeof options.port is 'number' or 'string'; any other port value falls through to the pipe branch which never reads port, so Node…

Verification: nit — real regression this PR introduces, but the triggering inputs (port: true, [8080], new Number(80)) are exotic; unlikely a real caller hits it. Mechanism traced and verified: src/js/node/_http_server.ts with listen({ port: true, path: sock }): - L548 port = arg0.port → true - L552 (port === undefined && "port" in arg0) || port === null → false, skipped - L574 `port ===…

@robobun

robobun commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on bun 1.4.3 (linux x64):

const s = Bun.serve({ port: 48123, unix: "/tmp/app.sock", fetch: () => new Response("ok") });
console.log(s.port, String(s.url)); // undefined unix:///tmp/app.sock, nothing listens on 48123

With this PR the call throws Cannot specify both port and unix. node:http's listen({ port, path }) and listen({ path, host }) follow Node's precedence and do not reach the throw.

The new tests in test/js/bun/http/serve.test.ts and test/js/node/http/node-http.test.ts fail on 1.4.3 and pass with the fix on linux x64 and Windows x64. They pass on every CI lane in build #111887.

CI is red on lanes this diff does not touch: test/regression/issue/10887.test.ts stalled in a parallel batch on debian x64 (a decorator transpile test, worker crashed), and test-crypto-dh-leak.js on x64-asan also fails on main. The rest passed on retry. The diff is ready for review.

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.

2 participants