Repository: denoland/deno
Pull request: feat(ext/http): Add `addr` to HttpServer

Adds an `addr` field to `HttpServer` to simplify the pattern `Deno.serve({ onListen({ port } => listenPort = port })`. This becomes: `const server = Deno.serve({}); port = server.addr.port`.

Changes:
 - Refactors `serve` overloads to split TLS out (in preparation for landing a place for the TLS SNI information)
 - Adds an `addr` field to `HttpServer` that matches the `addr` field of the corresponding `Deno.Listener`s.


Changed files:
filename: cli/tsc/dts/lib.deno.ns.d.ts
patch: @@ -6217,7 +6217,7 @@ declare namespace Deno {
    */
   export function gid(): number | null;
 
-  /** Information for a HTTP request.
+  /** Additional information for an HTTP request and its connection.
    *
    * @category HTTP Server
    */
@@ -6269,7 +6269,7 @@ declare namespace Deno {
     onError?: (error: unknown) => Response | Promise<Response>;
 
     /** The callback which is called when the server starts listening. */
-    onListen?: (params: { hostname: string; port: number }) => void;
+    onListen?: (localAddr: Deno.NetAddr) => void;
   }
 
   /** Additional options which are used when opening a TLS (HTTPS) server.
@@ -6304,6 +6304,14 @@ declare namespace Deno {
     handler: ServeHandler;
   }
 
+  /**
+   * @category HTTP Server
+   */
+  export interface ServeTlsInit {
+    /** The handler to invoke to process each incoming request. */
+    handler: ServeHandler;
+  }
+
   /** @category HTTP Server */
   export interface ServeUnixOptions {
     /** The unix domain socket path to listen on. */
@@ -6316,7 +6324,7 @@ declare namespace Deno {
     onError?: (error: unknown) => Response | Promise<Response>;
 
     /** The callback which is called when the server starts listening. */
-    onListen?: (params: { path: string }) => void;
+    onListen?: (localAddr: Deno.UnixAddr) => void;
   }
 
   /** Information for a unix domain socket HTTP request.
@@ -6353,12 +6361,16 @@ declare namespace Deno {
    *
    * @category HTTP Server
    */
-  export interface HttpServer extends AsyncDisposable {
+  export interface HttpServer<A extends Deno.Addr = Deno.Addr>
+    extends AsyncDisposable {
     /** A promise that resolves once server finishes - eg. when aborted using
      * the signal passed to {@linkcode ServeOptions.signal}.
      */
     finished: Promise<void>;
 
+    /** The local address this server is listening on. */
+    addr: A;
+
     /**
      * Make the server block the event loop from finishing.
      *
@@ -6395,7 +6407,7 @@ declare namespace Deno {
    *
    * @category HTTP Server
    */
-  export function serve(handler: ServeHandler): HttpServer;
+  export function serve(handler: ServeHandler): HttpServer<Deno.NetAddr>;
   /** Serves HTTP requests with the given option bag and handler.
    *
    * You can specify the socket path with `path` option.
@@ -6444,7 +6456,67 @@ declare namespace Deno {
   export function serve(
     options: ServeUnixOptions,
     handler: ServeUnixHandler,
-  ): HttpServer;
+  ): HttpServer<Deno.UnixAddr>;
+  /** Serves HTTP requests with the given option bag and handler.
+   *
+   * You can specify an object with a port and hostname option, which is the
+   * address to listen on. The default is port `8000` on hostname `"127.0.0.1"`.
+   *
+   * You can change the address to listen on using the `hostname` and `port`
+   * options. The below example serves on port `3000` and hostname `"0.0.0.0"`.
+   *
+   * ```ts
+   * Deno.serve(
+   *   { port: 3000, hostname: "0.0.0.0" },
+   *   (_req) => new Response("Hello, world")
+   * );
+   * ```
+   *
+   * You can stop the server with an {@linkcode AbortSignal}. The abort signal
+   * needs to be passed as the `signal` option in the options bag. The server
+   * aborts when the abort signal is aborted. To wait for the server to close,
+   * await the promise returned from the `Deno.serve` API.
+   *
+   * ```ts
+   * const ac = new AbortController();
+   *
+   * const server = Deno.serve(
+   *    { signal: ac.signal },
+   *    (_req) => new Response("Hello, world")
+   * );
+   * server.finished.then(() => console.log("Server closed"));
+   *
+   * console.log("Closing server...");
+   * ac.abort();
+   * ```
+   *
+   * By default `Deno.serve` prints the message
+   * `Listening on http://<hostname>:<port>/` on listening. If you like to
+   * change this behavior, you can specify a custom `onListen` callback.
+   *
+   * ```ts
+   * Deno.serve({
+   *   onListen({ port, hostname }) {
+   *     console.log(`Server started at http://${hostname}:${port}`);
+   *     // ... more info specific to your server ..
+   *   },
+   * }, (_req) => new Response("Hello, world"));
+   * ```
+   *
+   * To enable TLS you must specify the `key` and `cert` options.
+   *
+   * ```ts
+   * const cert = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n";
+   * const key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n";
+   * Deno.serve({ cert, key }, (_req) => new Response("Hello, world"));
+   * ```
+   *
+   * @category HTTP Server
+   */
+  export function serve(
+    options: ServeOptions,
+    handler: ServeHandler,
+  ): HttpServer<Deno.NetAddr>;
   /** Serves HTTP requests with the given option bag and handler.
    *
    * You can specify an object with a port and hostname option, which is the
@@ -6503,11 +6575,10 @@ declare namespace Deno {
    */
   export function serve(
     options:
-      | ServeOptions
       | ServeTlsOptions
       | (ServeTlsOptions & TlsCertifiedKeyOptions),
     handler: ServeHandler,
-  ): HttpServer;
+  ): HttpServer<Deno.NetAddr>;
   /** Serves HTTP requests with the given option bag.
    *
    * You can specify an object with the path option, which is the
@@ -6534,7 +6605,7 @@ declare namespace Deno {
    */
   export function serve(
     options: ServeUnixInit & ServeUnixOptions,
-  ): HttpServer;
+  ): HttpServer<Deno.UnixAddr>;
   /** Serves HTTP requests with the given option bag.
    *
    * You can specify an object with a port and hostname option, which is the
@@ -6563,10 +6634,39 @@ declare namespace Deno {
   export function serve(
     options:
       & ServeInit
+      & ServeOptions,
+  ): HttpServer<Deno.NetAddr>;
+  /** Serves HTTP requests with the given option bag.
+   *
+   * You can specify an object with a port and hostname option, which is the
+   * address to listen on. The default is port `8000` on hostname `"127.0.0.1"`.
+   *
+   * ```ts
+   * const ac = new AbortController();
+   *
+   * const server = Deno.serve({
+   *   port: 3000,
+   *   hostname: "0.0.0.0",
+   *   handler: (_req) => new Response("Hello, world"),
+   *   signal: ac.signal,
+   *   onListen({ port, hostname }) {
+   *     console.log(`Server started at http://${hostname}:${port}`);
+   *   },
+   * });
+   * server.finished.then(() => console.log("Server closed"));
+   *
+   * console.log("Closing server...");
+   * ac.abort();
+   * ```
+   *
+   * @category HTTP Server
+   */
+  export function serve(
+    options:
+      & ServeTlsInit
       & (
-        | ServeOptions
         | ServeTlsOptions
         | (ServeTlsOptions & TlsCertifiedKeyOptions)
       ),
-  ): HttpServer;
+  ): HttpServer<Deno.NetAddr>;
 }

filename: ext/http/00_serve.js
patch: @@ -552,7 +552,7 @@ function serve(arg1, arg2) {
     const path = listener.addr.path;
     return serveHttpOnListener(listener, signal, handler, onError, () => {
       if (options.onListen) {
-        options.onListen({ path });
+        options.onListen(listener.addr);
       } else {
         console.log(`Listening on ${path}`);
       }
@@ -593,19 +593,18 @@ function serve(arg1, arg2) {
     listenOpts.port = listener.addr.port;
   }
 
-  const onListen = (scheme) => {
-    // 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 = listenOpts.hostname == "0.0.0.0"
-      ? "localhost"
-      : listenOpts.hostname;
-    const port = listenOpts.port;
+  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" ? "localhost" : addr.hostname;
+  addr.hostname = hostname;
 
+  const onListen = (scheme) => {
     if (options.onListen) {
-      options.onListen({ hostname, port });
+      options.onListen(addr);
     } else {
-      console.log(`Listening on ${scheme}${hostname}:${port}/`);
+      console.log(`Listening on ${scheme}${addr.hostname}:${addr.port}/`);
     }
   };
 
@@ -625,7 +624,7 @@ function serveHttpOnListener(listener, signal, handler, onError, onListen) {
 
   onListen(context.scheme);
 
-  return serveHttpOn(context, callback);
+  return serveHttpOn(context, listener.addr, callback);
 }
 
 /**
@@ -641,10 +640,10 @@ function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
 
   onListen(context.scheme);
 
-  return serveHttpOn(context, callback);
+  return serveHttpOn(context, connection.localAddr, callback);
 }
 
-function serveHttpOn(context, callback) {
+function serveHttpOn(context, addr, callback) {
   let ref = true;
   let currentPromise = null;
 
@@ -700,6 +699,7 @@ function serveHttpOn(context, callback) {
   })();
 
   return {
+    addr,
     finished,
     async shutdown() {
       if (!context.closing && !context.closed) {

filename: tests/unit/serve_test.ts
patch: @@ -43,7 +43,10 @@ function onListen(
 }
 
 async function makeServer(
-  handler: (req: Request) => Response | Promise<Response>,
+  handler: (
+    req: Request,
+    info: Deno.ServeHandlerInfo,
+  ) => Response | Promise<Response>,
 ): Promise<
   {
     finished: Promise<void>;
@@ -432,7 +435,7 @@ Deno.test(async function httpServerRejectsOnAddrInUse() {
 Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
   const ac = new AbortController();
   const deferred = Promise.withResolvers<void>();
-  const listeningDeferred = Promise.withResolvers<void>();
+  const listeningDeferred = Promise.withResolvers<Deno.NetAddr>();
 
   const server = Deno.serve({
     handler: async (request, { remoteAddr }) => {
@@ -447,11 +450,13 @@ Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
     },
     port: servePort,
     signal: ac.signal,
-    onListen: onListen(listeningDeferred.resolve),
+    onListen: (addr) => listeningDeferred.resolve(addr),
     onError: createOnErrorCb(ac),
   });
 
-  await listeningDeferred.promise;
+  const addr = await listeningDeferred.promise;
+  assertEquals(addr.hostname, server.addr.hostname);
+  assertEquals(addr.port, server.addr.port);
   const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
     headers: { "connection": "close" },
   });
@@ -472,7 +477,7 @@ Deno.test(
   async function httpServerOnListener() {
     const ac = new AbortController();
     const deferred = Promise.withResolvers<void>();
-    const listeningDeferred = Promise.withResolvers<void>();
+    const listeningDeferred = Promise.withResolvers();
     const listener = Deno.listen({ port: servePort });
     const server = serveHttpOnListener(
       listener,
@@ -3812,7 +3817,7 @@ Deno.test(
     permissions: { run: true, read: true, write: true },
   },
   async function httpServerUnixDomainSocket() {
-    const { promise, resolve } = Promise.withResolvers<{ path: string }>();
+    const { promise, resolve } = Promise.withResolvers<Deno.UnixAddr>();
     const ac = new AbortController();
     const filePath = tmpUnixSocketPath();
     const server = Deno.serve(
@@ -3830,7 +3835,7 @@ Deno.test(
       },
     );
 
-    assertEquals(await promise, { path: filePath });
+    assertEquals((await promise).path, filePath);
     assertEquals(
       "hello world!",
       await curlRequest(["--unix-socket", filePath, "http://localhost"]),