Summary
--listen-socket=fd://3 never works in the TypeScript implementation. A correct, listening AF_UNIX socket passed on fd 3 is rejected and the process exits 1:
fd 3 is not a Unix socket: set ListenStream to a filesystem path in the .socket unit
Socket activation is therefore entirely non-functional in TypeScript. Go and Rust accept the same fd and serve on it.
Cause
ts/src/index.ts guards the adopted fd with:
if (typeof server.address() !== "string") {
console.error(`fd ${fd} is not a Unix socket: ...`);
process.exit(1);
}
server.address() returns a string for a server that bound a path itself, but for a socket adopted via listen({ fd }) it returns null — so the condition is true for every fd, valid or not.
Measured on Node 22:
| fd 3 contents |
server.address() |
listening AF_UNIX socket |
null |
| listening TCP socket |
{"address":"127.0.0.1","family":"IPv4","port":55281} |
So null is the success case, and the TCP case is distinguished by being an object with a port — not by being a non-string.
Reproduction
Pass a real listening Unix socket on fd 3:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(path); s.listen(5)
os.dup2(s.fileno(), 3, inheritable=True)
subprocess.run(["node", "ts/dist/index.js", "--listen-socket=fd://3", ...], pass_fds=(3,))
GO listening=True -> serving
RUST listening=True -> serving
TS listening=True -> rc=1 "fd 3 is not a Unix socket"
Affected implementation(s)
Expected behavior
A listening AF_UNIX socket on fd 3 is adopted and served, matching Go and Rust. A TCP socket on fd 3 is still rejected — that guard is the reason the check exists and must be preserved.
Suggested fix
Invert the test to match what Node actually returns:
const addr = server.address();
if (addr !== null && typeof addr === "object" && "port" in addr) {
// TCP socket on fd 3 — reject
}
Context
Introduced in #38 (PR #38's content landed on main via a40a07f), where the check was added to stop a unit with ListenStream=127.0.0.1:2375 silently reinstating a TCP listener. The intent was right; the discriminator was wrong, and nothing exercised the fd://3 path end to end — which is precisely the test gap #29 point 3 calls out.
Found while investigating #29.
Summary
--listen-socket=fd://3never works in the TypeScript implementation. A correct, listeningAF_UNIXsocket passed on fd 3 is rejected and the process exits 1:Socket activation is therefore entirely non-functional in TypeScript. Go and Rust accept the same fd and serve on it.
Cause
ts/src/index.tsguards the adopted fd with:server.address()returns a string for a server that bound a path itself, but for a socket adopted vialisten({ fd })it returnsnull— so the condition is true for every fd, valid or not.Measured on Node 22:
server.address()AF_UNIXsocketnull{"address":"127.0.0.1","family":"IPv4","port":55281}So
nullis the success case, and the TCP case is distinguished by being an object with aport— not by being a non-string.Reproduction
Pass a real listening Unix socket on fd 3:
Affected implementation(s)
Expected behavior
A listening
AF_UNIXsocket on fd 3 is adopted and served, matching Go and Rust. A TCP socket on fd 3 is still rejected — that guard is the reason the check exists and must be preserved.Suggested fix
Invert the test to match what Node actually returns:
Context
Introduced in #38 (PR #38's content landed on
mainvia a40a07f), where the check was added to stop a unit withListenStream=127.0.0.1:2375silently reinstating a TCP listener. The intent was right; the discriminator was wrong, and nothing exercised thefd://3path end to end — which is precisely the test gap #29 point 3 calls out.Found while investigating #29.