Repository: denoland/deno
Pull request: fix(ext/node): `createServer().listen().address()` returns port 0

## Summary

- Bind the TCP/TLS port synchronously in `Server.listen()` so that `address()` immediately returns the OS-assigned port, matching Node.js behavior
- Previously the port binding was deferred to `nextTick` via `Deno.serve()`, causing `address()` to return port 0 when using port 0 (auto-assign)
- Introduces `_listen()` method on `ServerImpl` (overridden by HTTPS `Server` for TLS) that calls `Deno.listen()`/`listenTls()` synchronously, then passes the pre-bound listener to `serveHttpOnListener()` in `_serve()`
- Errors (e.g. `EADDRINUSE`) are caught and emitted asynchronously via `nextTick`, matching Node.js behavior

Fixes #31758
Fixes #24427

## Test plan

- [x] Added unit test for HTTP: `address()` returns non-zero port immediately after `listen(0)`
- [x] Added unit test for HTTPS: same behavior with TLS server
- [x] All existing `unit_node::http_test` tests pass (77 total)
- [x] All existing `unit_node::https_test` tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -70,7 +70,8 @@ import {
 } from "ext:deno_node/internal/errors.ts";
 import { getTimerDuration } from "ext:deno_node/internal/timers.mjs";
 import { getIPFamily } from "ext:deno_node/internal/net.ts";
-import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.ts";
+import { serveHttpOnListener, upgradeHttpRaw } from "ext:deno_http/00_serve.ts";
+import { listen as listenDeno } from "ext:deno_net/01_net.js";
 import { headersEntries } from "ext:deno_fetch/20_headers.js";
 import { Response } from "ext:deno_fetch/23_response.js";
 import {
@@ -2185,6 +2186,7 @@ export class ServerImpl extends EventEmitter {
   #server: Deno.HttpServer;
   #unref = false;
   #ac?: AbortController;
+  #listener: Deno.Listener | null = null;
   #serveDeferred: ReturnType<typeof Promise.withResolvers<void>>;
   listening = false;
 
@@ -2234,16 +2236,32 @@ export class ServerImpl extends EventEmitter {
     if (hostname == "localhost") {
       hostname = "127.0.0.1";
     }
+
+    // Bind the port synchronously so that address() returns the actual
+    // port immediately after listen(), matching Node.js behavior.
+    try {
+      this.#listener = this._listen(hostname, port);
+    } catch (e) {
+      // Emit the error asynchronously, matching Node.js behavior.
+      this.#addr = { hostname, port } as Deno.NetAddr;
+      nextTick(() => this.emit("error", e));
+      return this;
+    }
+    const addr = this.#listener.addr as Deno.NetAddr;
     this.#addr = {
-      hostname,
-      port,
+      hostname: addr.hostname,
+      port: addr.port,
     } as Deno.NetAddr;
     this.listening = true;
     nextTick(() => this._serve());
 
     return this;
   }
 
+  _listen(hostname: string, port: number): Deno.Listener {
+    return listenDeno({ hostname, port });
+  }
+
   _serve() {
     const ac = new AbortController();
     const handler = (request: Request, info: Deno.ServeHandlerInfo) => {
@@ -2302,18 +2320,21 @@ export class ServerImpl extends EventEmitter {
       return;
     }
     this.#ac = ac;
+    const listener = this.#listener;
+    this.#listener = null;
+    if (!listener) {
+      return;
+    }
     try {
-      this.#server = serve(
-        {
-          handler: handler as Deno.ServeHandler,
-          ...this.#addr,
-          signal: ac.signal,
-          // @ts-ignore Might be any without `--unstable` flag
-          onListen: ({ port }) => {
-            this.#addr!.port = port;
-            this.emit("listening");
-          },
-          ...this._additionalServeOptions?.(),
+      this.#server = serveHttpOnListener(
+        listener,
+        ac.signal,
+        handler,
+        (_error) => {
+          return new Response("Internal Server Error", { status: 500 });
+        },
+        () => {
+          this.emit("listening");
         },
       );
     } catch (e) {
@@ -2365,6 +2386,12 @@ export class ServerImpl extends EventEmitter {
       }
     }
 
+    // Close pre-bound listener if _serve() hasn't consumed it yet.
+    if (this.#listener) {
+      this.#listener.close();
+      this.#listener = null;
+    }
+
     if (listening && this.#ac) {
       if (this.#server) {
         this.#server.shutdown();

filename: ext/node/polyfills/https.ts
patch: @@ -17,6 +17,7 @@ import { type ServerHandler, ServerImpl as HttpServer } from "node:http";
 import { validateObject } from "ext:deno_node/internal/validators.mjs";
 import { kEmptyObject } from "ext:deno_node/internal/util.mjs";
 import { Buffer } from "node:buffer";
+import { listenTls } from "ext:deno_net/02_tls.js";
 
 export class Server extends HttpServer {
   constructor(opts, requestListener?: ServerHandler) {
@@ -40,15 +41,20 @@ export class Server extends HttpServer {
     super(opts, requestListener);
   }
 
-  _additionalServeOptions() {
-    return {
-      cert: this._opts.cert instanceof Buffer
-        ? this._opts.cert.toString()
-        : this._opts.cert,
-      key: this._opts.key instanceof Buffer
-        ? this._opts.key.toString()
-        : this._opts.key,
-    };
+  _listen(hostname: string, port: number): Deno.Listener {
+    const cert = this._opts.cert instanceof Buffer
+      ? this._opts.cert.toString()
+      : this._opts.cert;
+    const key = this._opts.key instanceof Buffer
+      ? this._opts.key.toString()
+      : this._opts.key;
+    return listenTls({
+      hostname,
+      port,
+      cert,
+      key,
+      alpnProtocols: ["h2", "http/1.1"],
+    });
   }
 
   _encrypted = true;

filename: tests/unit_node/http_test.ts
patch: @@ -2281,6 +2281,22 @@ Deno.test("[node/http] Server.address() includes family property", async () => {
   }
 });
 
+// https://github.com/denoland/deno/issues/31758
+Deno.test("[node/http] address() returns assigned port immediately after listen()", async () => {
+  const server = http.createServer();
+  server.listen(0);
+
+  // address() should return the real port synchronously, not 0
+  const addr = server.address()!;
+  assert(typeof addr === "object");
+  assert(typeof addr.port === "number");
+  assert(addr.port > 0, `Expected port > 0, got ${addr.port}`);
+
+  const { promise, resolve } = Promise.withResolvers<void>();
+  server.close(() => resolve());
+  await promise;
+});
+
 Deno.test("[node/http] ServerResponse.writeEarlyHints", async () => {
   const { promise, resolve } = Promise.withResolvers<void>();
   const server = http.createServer((_req, res) => {

filename: tests/unit_node/https_test.ts
patch: @@ -51,6 +51,24 @@ Deno.test("[node/https] Server.address() includes family property", async () =>
   }
 });
 
+// https://github.com/denoland/deno/issues/31758
+Deno.test("[node/https] address() returns assigned port immediately after listen()", async () => {
+  const server = https.createServer({
+    cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+    key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+  });
+  server.listen(0);
+
+  // address() should return the real port synchronously, not 0
+  const addr = server.address() as AddressInfo;
+  assert(typeof addr.port === "number");
+  assert(addr.port > 0, `Expected port > 0, got ${addr.port}`);
+
+  const { promise, resolve } = Promise.withResolvers<void>();
+  server.close(() => resolve());
+  await promise;
+});
+
 Deno.test({
   name:
     "request.socket.authorized is true when successfully requested to https server",