Repository: denoland/deno
Pull request: fix(ext/http): do not set localhost to hostname unnecessarily

This PR changes when to cause the hostname substition of `0.0.0.0` -> `localhost`.

Currently we substitute `localhost` to the hostname on windows before calling `options.onListen`, which prevents the users to do more advanced thing using hostname string like https://github.com/denoland/std/issues/5558. This PR changes it not to substitute it when the user provide `onListen` callback.

closes #24776
unblocks https://github.com/denoland/std/issues/5558

Changed files:
filename: ext/http/00_serve.ts
patch: @@ -583,6 +583,19 @@ type RawServeOptions = {
 
 const kLoadBalanced = Symbol("kLoadBalanced");
 
+function mapAnyAddrToLocalhostForWindows(hostname: string) {
+  // If the hostname is "0.0.0.0", we display "localhost" in console
+  // because browsers in Windows don't resolve "0.0.0.0".
+  // See the discussion in https://github.com/denoland/deno_std/issues/1165
+  if (
+    (Deno.build.os === "windows") &&
+    (hostname == "0.0.0.0" || hostname == "::")
+  ) {
+    return "localhost";
+  }
+  return hostname;
+}
+
 function serve(arg1, arg2) {
   let options: RawServeOptions | undefined;
   let handler: RawHandler | undefined;
@@ -672,22 +685,15 @@ function serve(arg1, arg2) {
   }
 
   const addr = listener.addr;
-  // If the hostname is "0.0.0.0", we display "localhost" in console
-  // because browsers in Windows don't resolve "0.0.0.0".
-  // See the discussion in https://github.com/denoland/deno_std/issues/1165
-  const hostname = (addr.hostname == "0.0.0.0" || addr.hostname == "::") &&
-      (Deno.build.os === "windows")
-    ? "localhost"
-    : addr.hostname;
-  addr.hostname = hostname;
 
   const onListen = (scheme) => {
     if (options.onListen) {
       options.onListen(addr);
     } else {
-      const host = StringPrototypeIncludes(addr.hostname, ":")
-        ? `[${addr.hostname}]`
-        : addr.hostname;
+      const hostname = mapAnyAddrToLocalhostForWindows(addr.hostname);
+      const host = StringPrototypeIncludes(hostname, ":")
+        ? `[${hostname}]`
+        : hostname;
       // deno-lint-ignore no-console
       console.log(`Listening on ${scheme}${host}:${addr.port}/`);
     }
@@ -862,9 +868,10 @@ function registerDeclarativeServer(exports) {
             const nThreads = serveWorkerCount > 1
               ? ` with ${serveWorkerCount} threads`
               : "";
+            const hostname_ = mapAnyAddrToLocalhostForWindows(hostname);
             // deno-lint-ignore no-console
             console.debug(
-              `%cdeno serve%c: Listening on %chttp://${hostname}:${port}/%c${nThreads}`,
+              `%cdeno serve%c: Listening on %chttp://${hostname_}:${port}/%c${nThreads}`,
               "color: green",
               "color: inherit",
               "color: yellow",

filename: tests/unit/serve_test.ts
patch: @@ -4191,3 +4191,19 @@ Deno.test({
     'Operation `"op_net_listen_unix"` not supported on non-unix platforms.',
   );
 });
+
+Deno.test({
+  name: "onListen callback gets 0.0.0.0 hostname as is",
+}, async () => {
+  const { promise, resolve } = Promise.withResolvers<{ hostname: string }>();
+
+  const server = Deno.serve({
+    handler: (_) => new Response("ok"),
+    hostname: "0.0.0.0",
+    port: 0,
+    onListen: resolve,
+  });
+  const { hostname } = await promise;
+  assertEquals(hostname, "0.0.0.0");
+  await server.shutdown();
+});

filename: tests/unit_node/net_test.ts
patch: @@ -79,6 +79,7 @@ Deno.test("[node/net] net.connect().unref() works", async () => {
     port: 0, // any available port will do
     handler: () => new Response("hello"),
     onListen: async ({ port, hostname }) => {
+      hostname = Deno.build.os === "windows" ? "localhost" : hostname;
       const { stdout, stderr } = await new Deno.Command(Deno.execPath(), {
         args: [
           "eval",