Repository: denoland/deno
Pull request: fix: invalid ipv6 hostname on `deno serve`

This PR fixes an invalid URL being printed when running `deno serve`

Before: invalid URL

```sh
$ deno serve --host localhost
deno serve: Listening on http://::1:8000/
```

After: valid URL

```sh
$ deno serve --host localhost
deno serve: Listening on http://[::1]:8000/
```

Changed files:
filename: ext/http/00_serve.ts
patch: @@ -583,7 +583,7 @@ type RawServeOptions = {
 
 const kLoadBalanced = Symbol("kLoadBalanced");
 
-function mapAnyAddrToLocalhostForWindows(hostname: string) {
+function formatHostName(hostname: string): 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
@@ -593,7 +593,9 @@ function mapAnyAddrToLocalhostForWindows(hostname: string) {
   ) {
     return "localhost";
   }
-  return hostname;
+
+  // Add brackets around ipv6 hostname
+  return StringPrototypeIncludes(hostname, ":") ? `[${hostname}]` : hostname;
 }
 
 function serve(arg1, arg2) {
@@ -690,10 +692,8 @@ function serve(arg1, arg2) {
     if (options.onListen) {
       options.onListen(addr);
     } else {
-      const hostname = mapAnyAddrToLocalhostForWindows(addr.hostname);
-      const host = StringPrototypeIncludes(hostname, ":")
-        ? `[${hostname}]`
-        : hostname;
+      const host = formatHostName(addr.hostname);
+
       // deno-lint-ignore no-console
       console.log(`Listening on ${scheme}${host}:${addr.port}/`);
     }
@@ -868,10 +868,11 @@ function registerDeclarativeServer(exports) {
             const nThreads = serveWorkerCount > 1
               ? ` with ${serveWorkerCount} threads`
               : "";
-            const hostname_ = mapAnyAddrToLocalhostForWindows(hostname);
+            const host = formatHostName(hostname);
+
             // deno-lint-ignore no-console
             console.debug(
-              `%cdeno serve%c: Listening on %chttp://${hostname_}:${port}/%c${nThreads}`,
+              `%cdeno serve%c: Listening on %chttp://${host}:${port}/%c${nThreads}`,
               "color: green",
               "color: inherit",
               "color: yellow",

filename: tests/unit/serve_test.ts
patch: @@ -872,6 +872,36 @@ Deno.test({ permissions: { net: true } }, async function validPortString() {
   await server.shutdown();
 });
 
+Deno.test({ permissions: { net: true } }, async function ipv6Hostname() {
+  const ac = new AbortController();
+  let url = "";
+
+  const consoleLog = console.log;
+  console.log = (msg) => {
+    try {
+      const match = msg.match(/Listening on (http:\/\/(.*?):(\d+)\/)/);
+      assert(!!match, `Didn't match ${msg}`);
+      url = match[1];
+    } finally {
+      ac.abort();
+    }
+  };
+
+  try {
+    const server = Deno.serve({
+      handler: () => new Response(),
+      hostname: "::1",
+      port: 0,
+      signal: ac.signal,
+    });
+    assertEquals(server.addr.transport, "tcp");
+    assert(new URL(url), `Not a valid URL "${url}"`);
+    await server.shutdown();
+  } finally {
+    console.log = consoleLog;
+  }
+});
+
 Deno.test({ permissions: { net: true } }, function invalidPortFloat() {
   assertThrows(
     () =>