Skip to content

node:http: surface EINVAL for Server.listen({ fd }) instead of silently binding a random port - #33578

Open
robobun wants to merge 4 commits into
mainfrom
farm/779148ad/http-listen-fd-einval
Open

robobun wants to merge 4 commits into
mainfrom
farm/779148ad/http-listen-fd-einval

Conversation

@robobun

@robobun robobun commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #22559

What

http.Server.listen({ fd }) (socket activation / inherited listener: systemd LISTEN_FDS, inetd, a privilege-dropping supervisor) silently ignored the fd option. The listening event fired, but the server bound a fresh random ephemeral port on :: instead of adopting the already-bound descriptor. The process reported listening, health checks that poll the process passed, and the deployment was unreachable on the socket the supervisor was holding, with no error anywhere.

net.Server.listen({ fd }) already reports EINVAL for the same unsupported operation; only the node:http path failed silently.

Repro

// parent binds+listens a socket, hands it to the child as fd 3
import net from "node:net";
import http from "node:http";

const srv = http.createServer((req, res) => res.end("served-on-inherited-fd"));
srv.once("error", e => console.log("error", e.code));
srv.listen({ fd: 3 }, () => console.log("listening", srv.address()));
// before: listening { address: '::', family: 'IPv6', port: <RANDOM> }  -> unreachable
// after:  error EINVAL

Cause

Server.prototype.listen in src/js/node/_http_server.ts never read arg0.fd. With no port/path, the normalizer defaulted port to 0 and handed that to Bun.serve, which bound an ephemeral TCP port. Bun.serve has no way to adopt an existing listening descriptor (uSockets exposes no listen-from-fd entry point), so the option had nowhere to go.

Fix

Read fd from the options object and, when present, throw the same EINVAL (syscall: "listen") that net.Server.listen({ fd }) reports. The throw goes through listen's existing try/catch, so it surfaces as an error event, matching Node's listen error semantics and Bun's own node:net behavior. Callers now learn immediately that the socket was not adopted instead of silently serving on the wrong port.

Actually adopting an inherited listening fd would require a new native uSockets listen-from-fd path plus an Address::Fd variant in the server config; that is a larger, cross-cutting change worth a separate discussion.

Verification

test/js/node/http/node-http-listen-fd.test.ts: a donor net server provides a real listening fd; http.Server.listen({ fd }) must emit error with { code: "EINVAL", syscall: "listen" }.

  • Fails on current main (emits listening on a random port, e.g. :: 43749).
  • Passes with this change.

@github-actions github-actions Bot added the claude label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b526975-27cf-40aa-b590-4da79efa93d1

📥 Commits

Reviewing files that changed from the base of the PR and between 9f94ae3 and 42617ec.

📒 Files selected for processing (2)
  • src/js/node/_http_server.ts
  • test/js/node/http/node-http-listen-fd.test.ts

Walkthrough

Server.prototype.listen in the Node.js HTTP server compatibility layer now reads an fd field from the options object and throws an EINVAL error (errno -22, syscall "listen") when a numeric file descriptor is provided, instead of attempting to bind to it. A corresponding test was added.

Changes

fd rejection in http.Server.listen

Layer / File(s) Summary
Extract and validate fd option
src/js/node/_http_server.ts
Adds an fd local variable, extracts it from listen options, and throws an EINVAL error with errno and syscall set when a numeric fd >= 0 is passed.
Test coverage for fd rejection
test/js/node/http/node-http-listen-fd.test.ts
Adds a POSIX-only test that obtains a valid fd from a donor server and asserts listen({ fd }) fails with an EINVAL/listen error.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ServerListen as Server.prototype.listen
  Caller->>ServerListen: listen({ fd })
  ServerListen->>ServerListen: extract fd from arg0
  ServerListen->>ServerListen: check typeof fd === "number" && fd >= 0
  ServerListen-->>Caller: throw EINVAL (errno=-22, syscall="listen")
Loading

Related Issues: None found in provided context.

Related PRs: None found in provided context.

Suggested labels: node:http, bug, test

Suggested reviewers: None identified from provided context.

Poem
A rabbit sniffs a socket's fd,
"Not so fast," it hops to see.
EINVAL barks, the listen stops,
No binding here on borrowed props.
Tests confirm the guard holds true —
Hop along, this fd won't do! 🐇

🚥 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 states the node:http EINVAL behavior change for Server.listen({ fd }) and its user-visible effect.
Description check ✅ Passed The description covers what changed and how it was verified, with only minor heading differences from the template.

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

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. systemd socket activation doesn't work #22559 - Reports that systemd socket activation (LISTEN_FDS) silently fails because listen({ fd }) is ignored; this PR surfaces an explicit EINVAL error instead of silently binding a random port

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #22559

🤖 Generated with Claude Code

@robobun

robobun commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:24 AM PT - Jul 7th, 2026

❌ @robobun, your commit 42617ec has some failures in Build #69582 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 33578

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

bun-33578 --bun

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch, that's the same issue. Added Fixes #22559 to the description.

@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/http/node-http-listen-fd.test.ts`:
- Around line 20-27: The listen-fd test only checks the error event’s code and
syscall, but the updated error contract also includes errno. Update the
assertion in the node-http-listen-fd test around the server.once("error")
handling to capture errno from the emitted error and include it in the expected
object alongside code and syscall, so the test fully verifies the listen failure
shape.
🪄 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: 9853ef00-6f19-4683-a6ec-2a5f8e4a9e3a

📥 Commits

Reviewing files that changed from the base of the PR and between a12475e and 9f94ae3.

📒 Files selected for processing (2)
  • src/js/node/_http_server.ts
  • test/js/node/http/node-http-listen-fd.test.ts

Comment thread test/js/node/http/node-http-listen-fd.test.ts Outdated
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.

LGTM — the errno platform branch addresses my earlier note, and the guard is a clear improvement over silently binding a random port.

Extended reasoning...

Overview

Adds a guard in Server.prototype.listen (src/js/node/_http_server.ts) that reads fd from the options object and, when a numeric fd ≥ 0 is present, throws an EINVAL error inside the existing try/catch so it surfaces as an error event. Previously the option was ignored and the server silently bound a random ephemeral port. A new POSIX-gated test uses a donor net.Server to obtain a real listening fd and asserts the error shape (code, errno, syscall).

Security risks

None. This is a diagnostic-only guard that turns a silent misbehavior into an explicit, catchable error. No new I/O paths, no auth/crypto, no user-controlled data reaching a parser or syscall.

Level of scrutiny

Low. ~11 lines of production code adding an early-throw guard that routes through the existing error-emit path, plus a self-contained test. The change strictly narrows behavior on an input that was already unsupported — nothing that previously worked stops working. It also aligns node:http with what Bun's net.Server.listen({ fd }) already reports.

Other factors

  • My earlier inline nit about the hardcoded errno = -22 on Windows was addressed with the process.platform === "win32" ? -4071 : -22 branch, matching sibling built-ins.
  • CodeRabbit's suggestion to assert errno in the test was applied; both threads are resolved.
  • The bug-hunting pass found no issues.
  • The test awaits the actual condition (races error vs listening), cleans up in finally, and is skipped on non-POSIX where the errno differs.
  • No CODEOWNERS cover the touched paths.
  • The PR explicitly defers actual fd adoption (which would need a native uSockets listen-from-fd path) to a separate discussion; failing loudly is the right interim behavior.

@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 guard that turns a silent wrong-port bind into an explicit EINVAL, matching net.Server's behavior; both review nits addressed.

Extended reasoning...

Overview

This PR touches two files: a ~12-line addition to Server.prototype.listen in src/js/node/_http_server.ts that reads arg0.fd and throws an EINVAL-shaped error when a numeric fd ≥ 0 is passed, and a new POSIX-only test at test/js/node/http/node-http-listen-fd.test.ts that obtains a real listening fd from a donor net.Server and asserts the error event fires with { code: 'EINVAL', errno: -22, syscall: 'listen' }. The throw sits inside the existing try/catch, so it surfaces as an 'error' event rather than a synchronous throw — matching Node's listen-error semantics.

Security risks

None. No new input parsing, no auth/crypto/permissions surface, no external data handling. The only new branch inspects a user-supplied number and constructs a plain Error object.

Level of scrutiny

Low-to-moderate. node:http is production-critical, but this change is purely additive: it adds a guard on a code path that was previously broken (silently binding a random ephemeral port instead of the inherited socket, leaving the service unreachable — issue #22559). The new behavior is strictly better than silent failure and matches Bun's existing net.Server.listen({ fd }) EINVAL. No existing behavior is altered for callers who don't pass fd.

Other factors

  • Both prior review comments (my Windows errno nit → process.platform === 'win32' ? -4071 : -22, and CodeRabbit's suggestion to assert errno in the test) were addressed in commit eccae3a and the threads are resolved.
  • The bug-hunting system found no issues.
  • No CODEOWNERS cover these paths.
  • The test follows repo conventions (topic-specific file under test/js/node/http/, isPosix gate, donor server for a real fd, awaits the actual event, cleans up in finally).
  • The PR explicitly scopes out actual fd adoption (which would need a native uSockets listen-from-fd path) as a separate discussion; that's a reasonable call and doesn't block this stopgap.

@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 fix that brings http.Server.listen({ fd }) in line with net.Server's existing EINVAL behavior; prior feedback on the platform-specific errno has been addressed.

Extended reasoning...

Overview

This PR touches src/js/node/_http_server.ts (~15 lines) to read the fd option from Server.prototype.listen's options object and, when present, throw an EINVAL error (routed through the existing try/catch to an 'error' event) instead of silently defaulting port to 0 and binding a random ephemeral port. A new POSIX-gated test at test/js/node/http/node-http-listen-fd.test.ts obtains a real listening fd from a donor net.Server and asserts the error shape (code, errno, syscall).

Security risks

None. No parsing of untrusted input, no auth/crypto/permissions surface. The change only adds an early-exit guard on a user-supplied option value that was previously ignored.

Level of scrutiny

Low-to-moderate. This is Node.js compat-layer JS, not native code — no memory-safety or GC concerns. The behavior change (silent wrong-port bind → explicit 'error' event) is strictly an improvement over a documented silent failure (issue #22559), and it mirrors what net.Server.listen({ fd }) already does in Bun, so no new policy is being introduced. The PR explicitly defers actual fd adoption (which would need a native uSockets listen-from-fd path) to a separate discussion, which is the right scoping.

Other factors

  • Both prior review threads are resolved: my earlier note about the hardcoded POSIX errno was addressed in eccae3a (now process.platform === 'win32' ? -4071 : -22, matching the child_process.ts precedent), and CodeRabbit's suggestion to assert errno in the test was applied.
  • No CODEOWNERS coverage on the modified files.
  • The fd >= 0 gate matches Node's own isValidFd semantics (non-negative number) and correctly leaves fd: undefined / absent untouched so existing port/path callers are unaffected.
  • Test uses try/finally for donor cleanup, wires both 'error' and 'listening' to the awaited promise, and is skipped on Windows where the donor-fd fixture isn't meaningful.

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. Both builds (69490 and the re-roll 69582) passed the new test/js/node/http/node-http-listen-fd.test.ts and all other node-http tests.

The failing files are all in unrelated subsystems and differ between the two runs:

  • both runs: cli/install/bun-install-registry.test.ts, cli/update_interactive_install.test.ts, napi/napi.test.ts
  • run 1 only: js/sql/sql-onconnect-onclose-throw.test.ts (postgres container "database system is starting up"), bake/dev-and-prod.test.ts
  • run 2 only: cli/hot/hot.test.ts, cli/install/migration/complex-workspace.test.ts, js/bun/terminal/terminal-*.test.ts, js/node/zlib/leak.test.ts

This change only touches src/js/node/_http_server.ts (an early EINVAL guard in Server.prototype.listen) and adds one test; it cannot affect install/SQL/napi/terminal/zlib/bake.

Ready for a maintainer merge or manual retry on the red lanes.

@robobun

robobun commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

#44013 makes http.Server.listen({ fd }) serve on the descriptor, which is what #22559 asks for. If it merges, this guard is not needed.

One difference: the test here listens on the descriptor of a live net.Server of the same process and expects EINVAL. Node v26.3.0 gives EEXIST (errno -17, syscall listen) for that case, and so does #44013.

This PR stays open until a maintainer decides on #44013.

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.

systemd socket activation doesn't work

1 participant