Skip to content

socket: support listening on an inherited fd (socket activation) - #36491

Open
Properrr wants to merge 1 commit into
oven-sh:mainfrom
Properrr:claude/socket-activation-listen-fd
Open

Properrr wants to merge 1 commit into
oven-sh:mainfrom
Properrr:claude/socket-activation-listen-fd

Conversation

@Properrr

@Properrr Properrr commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #22559 — makes systemd socket activation actually work, rather than fail more loudly.

$ systemd-socket-activate --listen=127.0.0.1:3000 node ./build   # serves
$ systemd-socket-activate --listen=127.0.0.1:3000 bun  ./build   # hangs

Two different failures were behind this:

  • net.Server.listen({ fd }) returned a blanket EINVAL — Bun does not support listening on a file descriptor.
  • http.Server.listen({ fd }) never read fd at all. It bound a fresh ephemeral port on :: and emitted listening, so the process reported healthy and passed liveness checks while the socket the supervisor was holding never got accepted on — no error anywhere. This is the one the reporter hit, via the SvelteKit node adapter.

Relationship to #33578

@robobun's #33578 fixes the second half by making http report EINVAL like net does, noting "uSockets exposes no listen-from-fd entry point". That's accurate for the tree as it stands — so this PR adds that entry point and adopts the descriptor instead. It supersedes #33578 and touches the same test path; happy to rebase onto it or close this if you'd rather land the error-surfacing fix first.

Cause

uSockets could create listeners (us_socket_group_listen, us_socket_group_listen_unix) but not adopt one. There was precedent for connected sockets (us_socket_from_fd) and UDP (us_create_udp_socket_from_fd), just nothing for a TCP listener.

Fix

Adds us_socket_group_listen_from_fd alongside the existing two and plumbs it through the layers: uws HttpContext/App → C ABI (uws_app_listen_from_fd) → Bun.listen (UnixOrHost::Fd) → Bun.serve (Address::Fd) → node:http.

Three things worth reviewer attention:

  • Validation. The fd is checked with SO_ACCEPTCONN before adoption. Polling a regular file or a connected socket would never become acceptable, so without this an unusable descriptor hangs instead of reporting — the exact failure mode being fixed. ENOTSOCK (not a socket) and EINVAL (a socket, but not listening) are now distinguished.
  • Ownership. Transfers only once the listener is live. The paths that create their own fd close it on failure; this one leaves it with the caller, since the supervisor may still want it.
  • Address reporting. server.address() returns the descriptor's real bound address ({"address":"127.0.0.1","family":"IPv4","port":18760}), matching Node, because port/hostname are read back from the live listener rather than from config. Address::Fd contributes neither.

http3: true with an inherited fd skips the H3 listener with a warning — the inherited descriptor is a stream socket and QUIC needs its own UDP socket, which the supervisor did not hand us. Same shape as the existing unix-socket behaviour.

How did you verify your code works?

Real systemd, end to end, with the debug build:

$ systemd-socket-activate --listen=127.0.0.1:18760 bun-debug net-only.js
Listening on 127.0.0.1:18760 as 3.
Communication attempt on fd 3.
listening, address={"address":"127.0.0.1","family":"IPv4","port":18760}

$ curl http://127.0.0.1:18760/
served-on-inherited-fd

Before this change the same command reported address={"address":"::","port":40069} and curl hung.

Three tests in test/js/node/http/node-http-listen-fd.test.ts, covering http, net, and the rejection path. They don't require systemd: a donor server binds 127.0.0.1:0, and the child inherits its own copy of the descriptor as fd 3 via stdio — the same handoff systemd performs, so the child gets a distinct fd and there's no shared-ownership close. The child's listening callback is awaited rather than slept on.

Verified the tests are real by reverting the source changes and rebuilding — all 3 fail:

 0 pass  3 fail

The rejection test fails differently pre-fix (EINVAL from the old blanket error vs ENOTSOCK now), which is the added diagnostic precision.

With the fix: 3 pass. Regression run over node-http.test.ts + serve.test.ts: 413 pass, 2 skip, 1 fail. The one failure is server.requestIP > v6, which I confirmed fails identically on a clean tree (it binds ::1 but fetches localhost, which resolves to 127.0.0.1 first on this box) — pre-existing and unrelated. cargo clippy -p bun_runtime -p bun_uws_sys --no-deps is clean.

Not covered: executed on Linux x64 only. The C path is POSIX-shaped and SO_ACCEPTCONN exists on macOS and Winsock, but I have not run it on either, and the tests are skipIf(!isPosix).

Update on the Windows typing question I raised earlier: I flagged uncertainty about the getsockopt optlen type on Windows. Resolved by inspection — packages/bun-usockets/src/internal/networking/bsd.h includes ws2tcpip.h on Windows, which typedefs socklen_t as int, matching Winsock's getsockopt(SOCKET, int, int, char *, int *). The (char *) cast and socklen_t out-param are correct on both platforms, and struct bsd_addr_t in that same header already uses socklen_t unguarded. So this should compile and behave on Windows; it still has not been run there.

`listen({ fd })` could not adopt a descriptor that a supervisor had
already bound and listened on, so systemd socket activation did not work:

    systemd-socket-activate --listen=127.0.0.1:3000 node ./build   # serves
    systemd-socket-activate --listen=127.0.0.1:3000 bun  ./build   # hangs

`net.Server` surfaced a blanket "Bun does not support listening on a file
descriptor" EINVAL. `http.Server` was worse — it never read `fd` at all,
so it bound a fresh ephemeral port on `::` and emitted `listening`. The
process looked healthy while the socket the supervisor held was never
accepted on, with no error anywhere.

uSockets could create listeners but not adopt one, so add
`us_socket_group_listen_from_fd` next to the existing `listen`/`listen_unix`
and plumb it through: uws `HttpContext`/`App`, the C ABI, `Bun.listen`
(`UnixOrHost::Fd`), `Bun.serve` (`Address::Fd`), and `node:http`.

The fd is validated with SO_ACCEPTCONN before adoption. Polling a regular
file or a connected socket would never become acceptable, so without the
check an unusable descriptor would hang rather than report. ENOTSOCK and
EINVAL are now distinguished. Ownership transfers only once the listener
is live, unlike the paths that create their own fd.

`server.address()` reports the descriptor's real bound address, matching
Node, because port and hostname are read back from the live listener
rather than from config.

Fixes oven-sh#22559

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Adds socket activation support by adopting inherited listening file descriptors through the uSockets/uWebSockets layers, Bun’s server runtime, and Node-compatible listen({ fd }) APIs. POSIX tests cover HTTP, net, and invalid-descriptor cases.

Inherited listener support

Layer / File(s) Summary
Native listener adoption
packages/bun-usockets/src/context.c, packages/bun-usockets/src/libusockets.h, packages/bun-uws/src/HttpContext.h, packages/bun-uws/src/App.h, src/uws_sys/libuwsockets.cpp
Validates inherited listening descriptors, configures polling, creates listeners, and exposes the C/C++ fd-based listener APIs.
Rust uWebSockets bindings
src/uws_sys/SocketGroup.rs, src/uws_sys/App.rs
Adds Rust wrappers and FFI declarations for fd-based socket-group and application listening.
Runtime fd configuration and dispatch
src/runtime/server/ServerConfig.rs, src/runtime/server/mod.rs, src/runtime/server/server_body.rs, src/runtime/socket/Listener.rs
Parses fd, routes Address::Fd through listener creation, reports invalid descriptors, and derives address metadata from live listeners.
JavaScript API propagation and coverage
src/js/node/_http_server.ts, test/js/node/http/node-http-listen-fd.test.ts
Threads fd through Node-compatible listen paths and tests inherited HTTP/net listeners plus rejection of non-listening descriptors.

Suggested reviewers: robobun, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #22559 by adopting inherited listening fds through Bun/http/net, validating descriptors, and adding tests.
Out of Scope Changes check ✅ Passed The changes stay focused on inherited-fd socket activation and related address/error handling; no clear out-of-scope work stands out.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding inherited-fd listening support.
Description check ✅ Passed The description includes both required sections and gives clear implementation details plus verification steps.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/server/mod.rs (1)

2863-2891: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate Addr::Fd on http1 before calling listen_from_fd.

Address::Fd is accepted with {http3: true, http1: false}, but the inherited fd path silently destroys the HTTP/3 app and still attaches a listening TCP fd to the regular HTTP app. Match the TCP branch here so config validation is actually enforced.

🤖 Prompt for 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.

In `@src/runtime/server/mod.rs` around lines 2863 - 2891, Update the address
handling around the Addr enum and its downstream listen_from_fd path to reject
or avoid Addr::Fd when http1 is false, matching the existing TCP branch’s HTTP/1
validation. Preserve inherited-fd behavior only for configurations with http1
enabled, preventing HTTP/3-only configurations from attaching the fd to the
regular HTTP app.
🤖 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 `@src/runtime/server/mod.rs`:
- Around line 1856-1869: Update the Address::Fd branch in on_listen_failed() to
receive and preserve the native errno from fd-adopt validation instead of
hardcoding EINVAL. Convert it using bun_sys::Error::from_code(errno,
bun_sys::Tag::listen).to_system_error(), while retaining the existing failure
message and fd context so non-socket and non-listening descriptors report their
distinct error codes.

In `@src/runtime/server/ServerConfig.rs`:
- Around line 1263-1277: Update the fd parsing block in the server configuration
function to reject fd when hostname or unix has already selected an address,
before assigning Address::Fd(fd). Return an invalid-arguments error for these
conflicting combinations, preserving the existing nonnegative fd validation and
allowing fd alone.

In `@test/js/node/http/node-http-listen-fd.test.ts`:
- Around line 19-23: Close the parent’s donor listener immediately after
Bun.spawn inherits file descriptor 3 and before issuing the request, while
preserving the child process setup in the test. Ensure the success tests await
observable completion and route failure conditions to rejection so they cannot
race or hang.

---

Outside diff comments:
In `@src/runtime/server/mod.rs`:
- Around line 2863-2891: Update the address handling around the Addr enum and
its downstream listen_from_fd path to reject or avoid Addr::Fd when http1 is
false, matching the existing TCP branch’s HTTP/1 validation. Preserve
inherited-fd behavior only for configurations with http1 enabled, preventing
HTTP/3-only configurations from attaching the fd to the regular HTTP app.
🪄 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 Plus

Run ID: 892f5eae-4833-4a59-affc-a07bdacc042f

📥 Commits

Reviewing files that changed from the base of the PR and between bbe3f6a and 3e8b2f4.

📒 Files selected for processing (13)
  • packages/bun-usockets/src/context.c
  • packages/bun-usockets/src/libusockets.h
  • packages/bun-uws/src/App.h
  • packages/bun-uws/src/HttpContext.h
  • src/js/node/_http_server.ts
  • src/runtime/server/ServerConfig.rs
  • src/runtime/server/mod.rs
  • src/runtime/server/server_body.rs
  • src/runtime/socket/Listener.rs
  • src/uws_sys/App.rs
  • src/uws_sys/SocketGroup.rs
  • src/uws_sys/libuwsockets.cpp
  • test/js/node/http/node-http-listen-fd.test.ts

Comment thread src/runtime/server/mod.rs
Comment on lines +1856 to +1869
server_config::Address::Fd(fd) => {
let err = jsc::SystemError {
errno: bun_sys::SystemErrno::EINVAL as i32,
code: bun_core::String::static_("EINVAL").into(),
message: bun_core::String::create_format(format_args!(
"Failed to listen on file descriptor {fd}: not a bound, listening socket"
))
.into(),
syscall: bun_core::String::static_("listen").into(),
fd: *fd,
..Default::default()
};
err.to_error_instance(global)
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files and inspect the cited server code plus nearby address/listen helpers.
printf 'Tracked files around server/mod.rs:\n'
git ls-files | rg '(^|/)src/runtime/server/mod\.rs$|(^|/)Listener\.rs$|(^|/)context\.c$|(^|/)context\.h$|(^|/).*listen.*fd.*\.(c|cpp|h|hpp|ts)$' || true

printf '\nserver/mod.rs line count:\n'
wc -l src/runtime/server/mod.rs 2>/dev/null || true

printf '\nRelevant server/mod.rs sections:\n'
sed -n '1830,1885p' src/runtime/server/mod.rs
printf '\n'
rg -n "enum Address|impl Address|Address::Fd|get_url_as_string|fn listen\(|listen_with_config|listen_from_fd|on_listen_failed|SystemErrno|get_errno" src/runtime/server/mod.rs

printf '\nListener.rs candidates:\n'
fd -a 'Listener\.rs$|listener\.rs$|net.*server|server.*net' . | sed 's#^\./##' | head -80

Repository: oven-sh/bun

Length of output: 6759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'src/runtime/server/mod.rs: listen() Address dispatch\n'
sed -n '2870,3045p' src/runtime/server/mod.rs

printf '\nsrc/runtime/server/mod.rs: get_url_as_string fd handling\n'
sed -n '495,545p' src/runtime/server/mod.rs
printf '\n'
sed -n '1900,1945p' src/runtime/server/mod.rs

printf '\npackages/bun-usockets/src/context.c listen_from_fd snippets\n'
wc -l packages/bun-usockets/src/context.c
rg -n "listen_from_fd|us_socket_group_listen_from_fd|ERROR|ENOTSOCK|EINVAL|SO_ACCEPTCONN|errno" packages/bun-usockets/src/context.c
sed -n '1,120p' packages/bun-usockets/src/context.c
sed -n '120,230p' packages/bun-usockets/src/context.c

printf '\nsrc/runtime/socket/Listener.rs FD/errno handling\n'
rg -n "listen_from_fd|Addr::Fd|errno|Fd\\(|E::ENOTSOCK|E::EINVAL|get_errno|to_error" src/runtime/socket/Listener.rs
sed -n '1,260p' src/runtime/socket/Listener.rs

printf '\nRelevant test expectations\n'
sed -n '1,220p' test/js/node/http/node-http-listen-fd.test.ts

Repository: oven-sh/bun

Length of output: 41746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'context.c listen_from_fd full implementation:\n'
sed -n '427,475p' packages/bun-usockets/src/context.c

printf '\ncontext.c App::listen_from_fd calls around 350-420:\n'
sed -n '340,418p' packages/bun-usockets/src/context.c

printf '\nRead-only verifier extracting errno assignments and call path:\n'
python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bun-usockets/src/context.c")
src = p.read_text()
m = re.search(r"struct us_listen_socket_t \*us_socket_group_listen_from_fd\([^}]+(?:\n[^{}]+)*\n\}", src, re.S)
print("listeners:", bool(m))
if m:
    print(m.group(0))
    for label in ["SO_ACCEPTCONN", "BUN_USOCKETS_BLOCKING", "bsd_set_nonblocking", "save_set_sockopts", "listen_with_options"]:
        print(f"{label}: ", bool(re.search(label, src[m.start():m.end()], re.S)))
    for expr in re.findall(r"(int|static|int .*int)\s+([^;\n]+?\([^)]*\)),\s*saved_errno|= saved_errno|(\*error\s*=\s*saved_errno|errno\s*=\s*errno|error\s*=\s*errno|errno\s*=.*?)", m.group(0), re.S):
        print(expr)
PY

Repository: oven-sh/bun

Length of output: 5227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'bun-uws bindings for listen_from_fd signatures:\n'
rg -n "listen_from_fd|listen_from_fd_rc|LIBUS_SOCKET_ERROR|typedef.*LIBUS_SOCKET_DESCRIPTOR|fn listen_from_fd|listen_from_fd\\(" packages src -g '*.{rs,h,hpp,hpp,hxx,clang}' 2>/dev/null || true
rg -n "listen_from_fd" . --type rust --type h --type cpp --type c 2>/dev/null | head -200

printf '\nSearch server on_listen trampoline for errno args:\n'
rg -n "on_listen|on_listen_failed|us_socket.*listen|listen_from_fd.*error|us_socket.*listen_from_fd" src/runtime/server/mod.rs

Repository: oven-sh/bun

Length of output: 4635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'uws_sys/libuwsockets.cpp listen_from_fd wrapper:\n'
sed -n '516,538p' src/uws_sys/libuwsockets.cpp

printf '\nsrc/runtime/server/mod.rs on_listen trampoline:\n'
sed -n '3185,3208p' src/runtime/server/mod.rs

Repository: oven-sh/bun

Length of output: 2177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'pkg exports / generated wrapper availability:\n'
git ls-files | rg '(^|/)libuwsockets\.(h|cpp|c|h|hpp)$|(^|/)libuwsockets[0-9]*\.(h|cpp|c|h|hpp)$|(^|/)generated.*uw|(^|/)packages/bun-usockets/src/libuwsockets'

printf '\nRead-only search for uws_app_listen_from_fd declarations and error handling:\n'
rg -n "uws_app_listen_from_fd|listen_from_fd_rc|listen_from_fd.*error|handler\\(\([^\\n]+nullptr|app->listen_from_fd" packages src/uws_sys src/uws -g '*.{h,hpp,hxx,c,cpp,rs}' 2>/dev/null || true

Repository: oven-sh/bun

Length of output: 798


Preserve the real errno for Address::Fd listen failures.

Native fd-adopt validation already distinguishes ENOTSOCK from EINVAL, but Bun.serve()’s Address::Fd arm always reports EINVAL in on_listen_failed(). Wire the real errno through the fd listen failure path and report bun_sys::Error::from_code(errno, bun_sys::Tag::listen).to_system_error() so non-listening/non-socket descriptors get the expected ECODE distinction.

🤖 Prompt for 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.

In `@src/runtime/server/mod.rs` around lines 1856 - 1869, Update the Address::Fd
branch in on_listen_failed() to receive and preserve the native errno from
fd-adopt validation instead of hardcoding EINVAL. Convert it using
bun_sys::Error::from_code(errno, bun_sys::Tag::listen).to_system_error(), while
retaining the existing failure message and fd context so non-socket and
non-listening descriptors report their distinct error codes.

Comment on lines +1263 to +1277
if let Some(fd_js) = arg.get_truthy(global, "fd")? {
if fd_js.is_number() {
let fd = fd_js.to_int32();
if fd < 0 {
return Err(
global.throw_invalid_arguments(format_args!("Expected \"fd\" to be >= 0"))
);
}
args.address = Address::Fd(fd);
}
}
if global.has_exception() {
return Err(JsError::Thrown);
}

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Crash: hostname + fd (or unix + fd) reaches an unreachable!() panic.

This block unconditionally overwrites args.address with Address::Fd(fd) without checking whether has_hostname was already set or whether args.address is already Address::Unix. Later in this same function (unchanged code, ~line 1572), the no-baseURI branch does:

let hostname: &[u8] = if has_hostname {
    match &args.address {
        Address::Tcp { hostname, .. } => hostname.as_ref().unwrap().as_bytes(),
        _ => unreachable!(),
    }
} else { b"0.0.0.0" };

Calling Bun.serve({ hostname: "example.com", fd: 3, fetch }) (no baseURI) sets has_hostname = true, then fd parsing overwrites args.address to Address::Fd, so this hits unreachable!() and panics the whole process. The same silent-precedence problem applies to unix + fd (no error, unlike the explicit hostname+unix check a few lines above).

💥 Proposed fix: reject conflicting address options when `fd` is set
         if let Some(fd_js) = arg.get_truthy(global, "fd")? {
             if fd_js.is_number() {
                 let fd = fd_js.to_int32();
                 if fd < 0 {
                     return Err(
                         global.throw_invalid_arguments(format_args!("Expected \"fd\" to be >= 0"))
                     );
                 }
+                if has_hostname {
+                    return Err(global.throw_invalid_arguments(format_args!(
+                        "Cannot specify both hostname and fd",
+                    )));
+                }
+                if matches!(args.address, Address::Unix(_)) {
+                    return Err(global.throw_invalid_arguments(format_args!(
+                        "Cannot specify both unix and fd",
+                    )));
+                }
                 args.address = Address::Fd(fd);
             }
         }
🤖 Prompt for 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.

In `@src/runtime/server/ServerConfig.rs` around lines 1263 - 1277, Update the fd
parsing block in the server configuration function to reject fd when hostname or
unix has already selected an address, before assigning Address::Fd(fd). Return
an invalid-arguments error for these conflicting combinations, preserving the
existing nonnegative fd validation and allowing fd alone.

Comment on lines +19 to +23
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "fixture.js")],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe", fd],
});

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the donor listener after spawning the child.

The parent still polls the shared listener, so it can accept the fetch itself and leave it unanswered. This makes both success tests race and potentially hang. Close the parent copy after Bun.spawn has inherited fd 3, before issuing the request.

Proposed fix
   await using proc = Bun.spawn({
     cmd: [bunExe(), path.join(String(dir), "fixture.js")],
     env: bunEnv,
     stdio: ["ignore", "pipe", "pipe", fd],
   });
+  await new Promise<void>((resolve, reject) =>
+    donor.close(error => (error ? reject(error) : resolve())),
+  );
 
   try {
@@
   } finally {
     proc.kill();
-    donor.close();
   }

As per coding guidelines, tests must await observable conditions and wire failure paths to rejection.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "fixture.js")],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe", fd],
});
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "fixture.js")],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe", fd],
});
await new Promise<void>((resolve, reject) =>
donor.close(error => (error ? reject(error) : resolve())),
);
try {
@@
} finally {
proc.kill();
}
🤖 Prompt for 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.

In `@test/js/node/http/node-http-listen-fd.test.ts` around lines 19 - 23, Close
the parent’s donor listener immediately after Bun.spawn inherits file descriptor
3 and before issuing the request, while preserving the child process setup in
the test. Ensure the success tests await observable completion and route failure
conditions to rejection so they cannot race or hang.

Source: Coding guidelines

@robobun

robobun commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

Thank you for this work. #44013 implements the same feature on top of us_socket_group_listen_fd, the entry point that main has since #31829. It keeps the shape of this PR: Address::Fd through ServerConfig and node:http, and the id fd:N that lets --hot find the server. The commit credits you as co-author.

Two behaviours differ:

I did not close this PR. A maintainer decides which one lands. If #44013 misses a case that this PR handles, please say so there.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

systemd socket activation doesn't work

2 participants