Skip to content

node:net: throw ERR_SOCKET_BAD_PORT for Server.listen("") instead of binding an ephemeral port - #34083

Open
robobun wants to merge 2 commits into
mainfrom
farm/88157a00/net-listen-empty-string-port
Open

robobun wants to merge 2 commits into
mainfrom
farm/88157a00/net-listen-empty-string-port

Conversation

@robobun

@robobun robobun commented Jul 13, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

net.Server#listen("") (and any whitespace-only string) now throws ERR_SOCKET_BAD_PORT synchronously, matching Node.js, instead of silently binding a dual-stack ephemeral TCP listener on ::.

Reproduction

import net from "node:net";
const srv = net.createServer(() => {});
srv.listen("", () => {
  console.log("LISTENING:", JSON.stringify(srv.address()));
});

Node.js throws synchronously:

RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type string ('').

Bun (before this change) binds a world-reachable ephemeral port:

LISTENING: {"family":"IPv6","address":"::","port":33235}

Cause

Server.prototype.listen had an early coercion that converted a string argument via Number() before deciding whether it was a pipe path or a port. Since Number("") is 0, empty and whitespace-only strings were silently treated as port 0, so the server bound an ephemeral listener and validatePort never saw the bad value.

Node's normalizeArgs uses isPipeName() to make this distinction: a positional string is a pipe path only when isPipeName() is true; otherwise it becomes options.port and is passed to validatePort, which rejects empty and whitespace-only strings.

Fix

Remove the early Number() coercion and use isPipeName(port) (already defined in the file and used by normalizeArgs) to route the positional string argument. Empty and whitespace-only strings are not pipe names, so they reach validatePort which throws ERR_SOCKET_BAD_PORT. Numeric strings like "0" or "8080" continue to work (validatePort accepts them and they are coerced via port | 0).

Verification

  • New test in test/js/node/net/node-net-server.test.ts covers "", " ", " ", "\\t"
  • Fails on released bun (err is undefined, server is listening), passes with this change
  • listen({port: ""}), Socket#connect("") and listen("0") were already correct and remain so
  • Node parallel tests test-net-listen-invalid-port, test-net-server-listen-options, test-net-server-listen-path pass

[stamp-90s] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 5 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/net/node-net-server.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3cb104d60)

test/js/node/net/node-net-server.test.ts:
(pass) net.createServer listen > should throw when no port or path when using options [25.62ms]
(pass) net.createServer listen > should listen on IPv6 by default [150.88ms]
(pass) net.createServer listen > should listen on IPv4 [25.71ms]
(pass) net.createServer listen > should call listening [16.38ms]
(pass) net.createServer listen > should provide listening property [18.32ms]
(pass) net.createServer listen > should listen on localhost [16.94ms]
(pass) net.createServer listen > should listen on localhost [17.18ms]
(pass) net.createServer listen > should listen without port or host [10.45ms]
(pass) net.createServer listen > should listen on unix domain socket [21.21ms]
(pas
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (9337493d0)

test/js/node/net/node-net-server.test.ts:
(pass) net.createServer listen > should throw when no port or path when using options [0.50ms]
(pass) net.createServer listen > should listen on IPv6 by default [3.17ms]
(pass) net.createServer listen > should listen on IPv4 [1.26ms]
(pass) net.createServer listen > should call listening [1.21ms]
(pass) net.createServer listen > should provide listening property [1.24ms]
(pass) net.createServer listen > should listen on localhost [1.26ms]
(pass) net.createServer listen > should listen on localhost [1.27ms]
(pass) net.createServer listen > should listen without port or host [1.27ms]
(pass) net.createServer listen > should listen on unix domain socket [1.43ms]
(pass) net.createServer listen > should bind IPv4 0.0.0.0 when listen on 0.0.0.0, issue#7355 [1.55ms]
(pass) net.createServer events > should receive data [3.99ms]
(pass) net.createServer events > should call end [1.75ms]
(pass) net.createServer events > should call close [0.17ms]
(pass) net.createServer events > should call connection and drop [1.88ms]
(pass) net.createServer events > should error on an invalid port [0.10ms]
(pass) 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/net/node-net-server.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3cb104d60)

test/js/node/net/node-net-server.test.ts:
(pass) net.createServer listen > should throw when no port or path when using options [25.90ms]
(pass) net.createServer listen > should listen on IPv6 by default [150.80ms]
(pass) net.createServer listen > should listen on IPv4 [33.21ms]
(pass) net.createServer listen > should call listening [17.94ms]
(pass) net.createServer listen > should provide listening property [26.98ms]
(pass) net.createServer listen > should listen on localhost [33.39ms]
(pass) net.createServer listen > should listen on localhost [38.57ms]
(pass) net.createServer listen > should listen without port or host [33.66ms]
(pass) net.createServer listen > should listen on unix domain socket [24.66ms]
(pas
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 823ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/21] gen cpp.rs (cppbind)
[2/21] gen JS modules (bundle-modules)
Preprocess modules (9258ms)
Bundle modules (72ms)
Postprocesss modules (27ms)
Bundle Functions (656ms)
Generate Code (85ms)

[10.11s] Bundled "src/js" for production
  1912 kb
  162 internal modules
  12 native modules
  90 internal functions across 19 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for 
... (truncated)
diff hotspot
src/js/node/net.ts                       | 10 ++++------
 src/jsc/bindings/NodeValidator.cpp       |  1 +
 test/js/node/net/node-net-server.test.ts | 23 +++++++++++++++++++++++
 3 files changed, 28 insertions(+), 6 deletions(-)

gate history · 2 passed · 0 rejected · iteration 0

evidence per changed file
file                                      reads  edits  tests
src/js/node/net.ts                            2      1      0
src/jsc/bindings/NodeValidator.cpp            2      1      0
test/js/node/net/node-net-server.test.ts      2      2      0

…binding an ephemeral port

Server.prototype.listen had an early coercion that converted any string
argument via Number() before deciding whether it was a pipe path or a
port. Since Number("") === 0, an empty (or whitespace-only) string was
silently treated as port 0 and the server bound a dual-stack ephemeral
listener on ::, instead of throwing ERR_SOCKET_BAD_PORT as Node does.

Replace the early coercion with the same isPipeName() discrimination
Node's normalizeArgs uses: a positional string is a pipe path only when
isPipeName() is true, otherwise it is passed to validatePort() (which
already rejects empty and whitespace-only strings in C++).
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Node port validation

Layer / File(s) Summary
Listen argument classification
src/js/node/net.ts
Server.listen treats positional strings as pipe paths only when isPipeName accepts them; other strings proceed to port validation.
Whitespace validation and regression coverage
src/jsc/bindings/NodeValidator.cpp, test/js/node/net/node-net-server.test.ts
Port trimming recognizes U+3000 whitespace, with parameterized tests covering whitespace-only inputs and synchronous ERR_SOCKET_BAD_PORT failures.

Suggested reviewers: jarred-sumner, cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: rejecting empty string listen ports with ERR_SOCKET_BAD_PORT.
Description check ✅ Passed The description includes the required purpose and verification sections, plus useful reproduction, cause, fix, and test details.

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

@robobun

robobun commented Jul 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:12 PM PT - Jul 13th, 2026

❌ @robobun, your commit 3cb104d has 2 failures in Build #72526 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34083

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

bun-34083 --bun

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

LGTM — small, well-scoped Node compat fix that swaps the hand-rolled Number() coercion for the existing isPipeName() helper, matching Node's normalizeArgs. Both inline notes are pre-existing sibling issues (in _http_server.ts and NodeValidator.cpp) that this PR doesn't touch; they're worth follow-ups but don't block here.

Extended reasoning...

Overview

The PR removes an early Number(port) coercion in Server.prototype.listen (src/js/node/net.ts) and replaces the typeof port === "string" pipe-path check with isPipeName(port), which is the same helper Node's normalizeArgs uses and which is already defined and used elsewhere in this file. Empty and whitespace-only strings now flow to the existing validatePort call at L3365-3369 and throw ERR_SOCKET_BAD_PORT instead of being coerced to port 0. A parametrized test in test/js/node/net/node-net-server.test.ts covers "", " ", " ", "\t".

I traced the other input classes through the new routing: numeric strings ("0", "8080") → isPipeName false → validatePort accepts → port | 0; real paths ("/tmp/sock") → isPipeName true → pipe branch; negative-number strings ("-1") → isPipeName true → pipe branch, which matches Node's toNumber semantics exactly; numbers/objects/functions/null/undefined are unaffected since isPipeName gates on typeof s === "string" first, same as the old check.

Security risks

None introduced. The change tightens validation — previously an empty string opened a world-reachable ephemeral listener on ::; now it throws synchronously. No new surface area.

Level of scrutiny

Low-to-medium. This is a ~7-line net change in the Node-compat argument-parsing path, reusing an existing in-file helper to match Node's reference implementation. No native code, no new state, no lifecycle changes.

Other factors

Both inline findings from the bug hunt are explicitly flagged pre-existing: (1) http.Server.listen("") has a separate listen() in _http_server.ts that never delegates here, and (2) validatePort's C++ trim omits U+3000. Neither is touched by this PR and both behave byte-identically before and after. They're reasonable follow-ups per the "fix the whole class" guidance but shouldn't block a correct, tested fix to the net.Server path.

Comment thread src/js/node/net.ts
Comment thread src/js/node/net.ts
The hand-rolled trim in jsFunction_validatePort enumerated the Unicode
Space_Separator range but omitted U+3000, so a string containing only
U+3000 was not considered empty-after-trim. Since JSC's ToNumber does
treat U+3000 as StrWhiteSpace, Number("\u3000") is 0 and validatePort
accepted it, so net.Server#listen("\u3000") bound an ephemeral port
instead of throwing ERR_SOCKET_BAD_PORT as Node does.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/net/node-net-server.test.ts`:
- Around line 474-495: Change the parameterized test from it.each() to
describe.each(), using each port value as the nested suite parameter and placing
the existing assertions and cleanup in a nested it(). Preserve the current test
behavior and error expectations for every port value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b14d1593-8b24-482b-b117-0531ca914fca

📥 Commits

Reviewing files that changed from the base of the PR and between 7d44148 and 3cb104d.

📒 Files selected for processing (3)
  • src/js/node/net.ts
  • src/jsc/bindings/NodeValidator.cpp
  • test/js/node/net/node-net-server.test.ts

Comment thread test/js/node/net/node-net-server.test.ts

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

LGTM — small, well-scoped Node-compat fix with good coverage.

Checked: isPipeName() routing preserves behavior for numeric strings ("0", "8080"), pipe paths, numbers, null/undefined, and options objects — only empty/whitespace strings now correctly reach validatePort. The one incidental change ("-1" now treated as a pipe name instead of throwing) matches Node's normalizeArgs exactly. The U+3000 addition to validatePort's trim set completes the ES StrWhiteSpace Zs range. Both prior review threads are addressed (U+3000 folded in; http.Server sibling deferred to a follow-up).

Extended reasoning...

Overview

Three files touched: src/js/node/net.ts (removes an early Number() coercion of the positional port argument in Server.prototype.listen and replaces the typeof port === "string" pipe-path check with isPipeName(port), which is the same helper Node's normalizeArgs uses and is already defined and used elsewhere in this file), src/jsc/bindings/NodeValidator.cpp (adds U+3000 IDEOGRAPHIC SPACE to jsFunction_validatePort's hand-rolled whitespace trim, one line matching the surrounding range-check style), and a new it.each test covering "", " ", " ", "\\t", "\\u3000".

Security risks

None introduced; this is a net security improvement. Before, listen("") silently bound a dual-stack world-reachable ephemeral TCP listener on :: — now it throws ERR_SOCKET_BAD_PORT synchronously before any socket is created, matching Node. The C++ change closes the same gap for U+3000-only strings.

Level of scrutiny

Moderate — this is user-facing argument routing in node:net, so I traced every input class through the new branch: numbers, null, undefined, functions, options objects, numeric strings ("0", "8080", " 8080 ", "1.5", "Infinity"), non-numeric strings (pipe paths), and empty/whitespace strings. All reach the same branch as before except the intended fix (empty/whitespace → validatePort throws) and one incidental case: negative numeric strings like "-1" now route to the pipe-path branch instead of throwing ERR_SOCKET_BAD_PORT. That is exactly Node's behavior (Node's toNumber returns false for negatives, so isPipeName("-1") is true), so it is a compat improvement, not a regression.

Other factors

  • isPipeName and toNumber are pre-existing, already used by normalizeArgs in the same file — this reuses the established helper rather than adding new logic.
  • Gate evidence in the PR description shows the new tests fail on main (both ASAN debug and release) and pass with the fix.
  • Both of my prior inline notes are resolved: the U+3000 gap was folded into this PR with a test case; the http.Server sibling in _http_server.ts was acknowledged and deferred to a follow-up (reasonable — that file has a separate listen() backed by Bun.serve with its own error-handling shape).
  • The 3-line comment in net.ts explains the routing intent and stays within the repo's comment-length guidance.

@robobun

robobun commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

CI build #72526: 284/286 lanes passed. The two red lanes are unrelated to this diff:

  • test/integration/next-pages/test/next-build.test.ts on alpine 3.23 x64: SIGSEGV in napi_release_threadsafe_function → enqueue_task_concurrent → platform_loop_opt from @next/swc-linux-x64-musl on a tokio worker thread. A napi TSF / VM-teardown race; being tracked separately.
  • test/js/third_party/grpc-js/test-tonic.test.ts on darwin 14 aarch64: marked pre-existing on main by the CI tooling.

The new ERR_SOCKET_BAD_PORT tests passed on every lane. The one node-net-server.test.ts retry on debian aarch64 was the pre-existing "should listen on unix domain socket" test's 100ms setTimeout guard (added 2023, unchanged here) and passed on retry.

Ready for review.

steipete added a commit to openclaw/bun that referenced this pull request Oct 4, 2026
Validate listen ports before DNS and binding so HTTP and HTTPS argument errors throw synchronously, while real bind failures remain asynchronous. Preserve numeric-string validation and Node pipe-name routing in net.

Adapts oven-sh#34083; thanks @robobun. Covers the OpenClaw gateway listen regression. Node 24 oracle, Linux regression/consumer proof, scoped P2 review, and both native CI lanes passed on the guarded head.

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