Skip to content

TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44

Closed

Description

@abienkowski

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)

  • Go
  • Rust
  • TypeScript

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.

Activity

  1. added
    Priority: P1Added to issues and PRs relating to a high severity bugs.
    Type: BugAdded to issues and PRs if they are addressing a bug
    on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: P1Added to issues and PRs relating to a high severity bugs.Type: BugAdded to issues and PRs if they are addressing a bug

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions