From 051ab73026d47d2ba3e81abaf03d696c8756f479 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Sat, 22 Jun 2024 15:47:02 +1000 Subject: [PATCH] BREAKING(http): remove deprecated `server` module (#5100) --- .github/dependency_graph.svg | 360 ++++---- http/deno.json | 1 - http/mod.ts | 1 - http/server.ts | 905 ------------------- http/server_test.ts | 1631 ---------------------------------- 5 files changed, 177 insertions(+), 2721 deletions(-) delete mode 100644 http/server.ts delete mode 100644 http/server_test.ts diff --git a/.github/dependency_graph.svg b/.github/dependency_graph.svg index e469726e2..b6e00d7c5 100644 --- a/.github/dependency_graph.svg +++ b/.github/dependency_graph.svg @@ -4,457 +4,451 @@ - + std_deps - + archive - -archive + +archive io - -io + +io archive->io - - + + bytes - -bytes + +bytes - + io->bytes - - + + assert - -assert + +assert internal - -internal + +internal assert->internal - - + + async - -async + +async cli - -cli + +cli collections - -collections + +collections crypto - -crypto + +crypto csv - -csv + +csv streams - -streams + +streams csv->streams - - + + - + streams->bytes - - + + data-\nstructures - -data- -structures + +data- +structures datetime - -datetime + +datetime dotenv - -dotenv + +dotenv encoding - -encoding + +encoding expect - -expect + +expect expect->assert - - + + expect->internal - - + + fmt - -fmt + +fmt front-\nmatter - -front- -matter + +front- +matter toml - -toml + +toml front-\nmatter->toml - - + + yaml - -yaml + +yaml front-\nmatter->yaml - - + + - + toml->collections - - + + fs - -fs + +fs path - -path + +path fs->path - - + + html - -html + +html http - -http - - - -http->async - - + +http - + http->cli - - + + - + http->streams - - + + http->encoding - - + + - + http->fmt - - + + - + http->path - - + + media-\ntypes - -media- -types + +media- +types - + http->media-\ntypes - - + + net - -net + +net - + http->net - - + + ini - -ini + +ini json - -json + +json - + json->streams - - + + jsonc - -jsonc + +jsonc - + jsonc->json - - + + log - -log + +log - + log->io - - + + - + log->fmt - - + + - + log->fs - - + + msgpack - -msgpack + +msgpack - + msgpack->bytes - - + + regexp - -regexp + +regexp semver - -semver + +semver testing - -testing + +testing - + testing->assert - - + + - + testing->internal - - + + - + testing->async - - + + - + testing->data-\nstructures - - + + - + testing->fmt - - + + - + testing->fs - - + + - + testing->path - - + + text - -text + +text ulid - -ulid + +ulid url - -url + +url - + url->path - - + + uuid - -uuid + +uuid - + uuid->bytes - - + + - + uuid->crypto - - + + webgpu - -webgpu + +webgpu diff --git a/http/deno.json b/http/deno.json index c059ad7a2..4610cd306 100644 --- a/http/deno.json +++ b/http/deno.json @@ -7,7 +7,6 @@ "./etag": "./etag.ts", "./file-server": "./file_server.ts", "./negotiation": "./negotiation.ts", - "./server": "./server.ts", "./server-sent-event-stream": "./server_sent_event_stream.ts", "./status": "./status.ts", "./unstable-signed-cookie": "./unstable_signed_cookie.ts", diff --git a/http/mod.ts b/http/mod.ts index 2b6c63265..41a2b9d5c 100644 --- a/http/mod.ts +++ b/http/mod.ts @@ -66,7 +66,6 @@ export * from "./cookie.ts"; export * from "./etag.ts"; export * from "./status.ts"; export * from "./negotiation.ts"; -export * from "./server.ts"; export * from "./unstable_signed_cookie.ts"; export * from "./server_sent_event_stream.ts"; export * from "./user_agent.ts"; diff --git a/http/server.ts b/http/server.ts deleted file mode 100644 index 4aa01ab11..000000000 --- a/http/server.ts +++ /dev/null @@ -1,905 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { delay } from "@std/async/delay"; - -/** Thrown by Server after it has been closed. */ -const ERROR_SERVER_CLOSED = "Server closed"; - -/** Default port for serving HTTP. */ -const HTTP_PORT = 80; - -/** Default port for serving HTTPS. */ -const HTTPS_PORT = 443; - -/** Initial backoff delay of 5ms following a temporary accept failure. */ -const INITIAL_ACCEPT_BACKOFF_DELAY = 5; - -/** Max backoff delay of 1s following a temporary accept failure. */ -const MAX_ACCEPT_BACKOFF_DELAY = 1000; - -/** - * Information about the connection a request arrived on. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeHandlerInfo} instead. - */ -export interface ConnInfo { - /** The local address of the connection. */ - readonly localAddr: Deno.Addr; - /** The remote address of the connection. */ - readonly remoteAddr: Deno.Addr; -} - -/** - * A handler for HTTP requests. Consumes a request and connection information - * and returns a response. - * - * If a handler throws, the server calling the handler will assume the impact - * of the error is isolated to the individual request. It will catch the error - * and close the underlying connection. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeHandler} instead. - */ -export type Handler = ( - request: Request, - connInfo: ConnInfo, -) => Response | Promise; - -/** - * Options for running an HTTP server. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeInit} instead. - */ -export interface ServerInit extends Partial { - /** The handler to invoke for individual HTTP requests. */ - handler: Handler; - - /** - * The handler to invoke when route handlers throw an error. - * - * The default error handler logs and returns the error in JSON format. - */ - onError?: (error: unknown) => Response | Promise; -} - -/** - * Used to construct an HTTP server. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.serve} instead. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const port = 4505; - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ port, handler }); - * ``` - */ -export class Server { - #port?: number; - #host?: string; - #handler: Handler; - #closed = false; - #listeners: Set = new Set(); - #acceptBackoffDelayAbortController = new AbortController(); - #httpConnections: Set = new Set(); - #onError: (error: unknown) => Response | Promise; - - /** - * Constructs a new HTTP Server instance. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const port = 4505; - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ port, handler }); - * ``` - * - * @param serverInit Options for running an HTTP server. - */ - constructor(serverInit: ServerInit) { - this.#port = serverInit.port; - this.#host = serverInit.hostname; - this.#handler = serverInit.handler; - this.#onError = serverInit.onError ?? - function (error: unknown) { - console.error(error); - return new Response("Internal Server Error", { status: 500 }); - }; - } - - /** - * Accept incoming connections on the given listener, and handle requests on - * these connections with the given handler. - * - * HTTP/2 support is only enabled if the provided Deno.Listener returns TLS - * connections and was configured with "h2" in the ALPN protocols. - * - * Throws a server closed error if called after the server has been closed. - * - * Will always close the created listener. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ handler }); - * const listener = Deno.listen({ port: 4505 }); - * - * console.log("server listening on http://localhost:4505"); - * - * await server.serve(listener); - * ``` - * - * @param listener The listener to accept connections from. - */ - async serve(listener: Deno.Listener): Promise { - if (this.#closed) { - throw new Deno.errors.Http(ERROR_SERVER_CLOSED); - } - - this.#trackListener(listener); - - try { - return await this.#accept(listener); - } finally { - this.#untrackListener(listener); - - try { - listener.close(); - } catch { - // Listener has already been closed. - } - } - } - - /** - * Create a listener on the server, accept incoming connections, and handle - * requests on these connections with the given handler. - * - * If the server was constructed without a specified port, 80 is used. - * - * If the server was constructed with the hostname omitted from the options, the - * non-routable meta-address `0.0.0.0` is used. - * - * Throws a server closed error if the server has been closed. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const port = 4505; - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ port, handler }); - * - * console.log("server listening on http://localhost:4505"); - * - * await server.listenAndServe(); - * ``` - */ - async listenAndServe(): Promise { - if (this.#closed) { - throw new Deno.errors.Http(ERROR_SERVER_CLOSED); - } - - const listener = Deno.listen({ - port: this.#port ?? HTTP_PORT, - hostname: this.#host ?? "0.0.0.0", - transport: "tcp", - }); - - return await this.serve(listener); - } - - /** - * Create a listener on the server, accept incoming connections, upgrade them - * to TLS, and handle requests on these connections with the given handler. - * - * If the server was constructed without a specified port, 443 is used. - * - * If the server was constructed with the hostname omitted from the options, the - * non-routable meta-address `0.0.0.0` is used. - * - * Throws a server closed error if the server has been closed. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const port = 4505; - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ port, handler }); - * - * const certFile = "/path/to/certFile.crt"; - * const keyFile = "/path/to/keyFile.key"; - * - * console.log("server listening on https://localhost:4505"); - * - * await server.listenAndServeTls(certFile, keyFile); - * ``` - * - * @param certFile The path to the file containing the TLS certificate. - * @param keyFile The path to the file containing the TLS private key. - */ - async listenAndServeTls(certFile: string, keyFile: string): Promise { - if (this.#closed) { - throw new Deno.errors.Http(ERROR_SERVER_CLOSED); - } - - const listener = Deno.listenTls({ - port: this.#port ?? HTTPS_PORT, - hostname: this.#host ?? "0.0.0.0", - cert: Deno.readTextFileSync(certFile), - key: Deno.readTextFileSync(keyFile), - transport: "tcp", - // ALPN protocol support not yet stable. - // alpnProtocols: ["h2", "http/1.1"], - }); - - return await this.serve(listener); - } - - /** - * Immediately close the server listeners and associated HTTP connections. - * - * Throws a server closed error if called after the server has been closed. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ handler }); - * const listener = Deno.listen({ port: 4505 }); - * - * console.log("server listening on http://localhost:4505"); - * - * const serve = server.serve(listener); - * setTimeout(() => { - * server.close(); - * }, 1000); - * await serve; - * ``` - */ - close() { - if (this.#closed) { - throw new Deno.errors.Http(ERROR_SERVER_CLOSED); - } - - this.#closed = true; - - for (const listener of this.#listeners) { - try { - listener.close(); - } catch { - // Listener has already been closed. - } - } - - this.#listeners.clear(); - - this.#acceptBackoffDelayAbortController.abort(); - - for (const httpConn of this.#httpConnections) { - this.#closeHttpConn(httpConn); - } - - this.#httpConnections.clear(); - } - - /** - * Get whether the server is closed. - * - * @example Usage - * ```ts no-eval - * import { Server } from "@std/http/server"; - * - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ handler }); - * const listener = Deno.listen({ port: 4505 }); - * - * console.log("server listening on http://localhost:4505"); - * - * const serve = server.serve(listener); - * setTimeout(() => { - * server.close(); - * }, 1000); - * await serve; - * console.log(server.closed); // returns true - * ``` - * - * @returns Whether its closed or not. - */ - get closed(): boolean { - return this.#closed; - } - - /** - * Get the list of network addresses the server is listening on. - * - * @example Usage - * ```tsm no-eval - * import { Server } from "@std/http/server"; - * - * const handler = (request: Request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }; - * - * const server = new Server({ handler }); - * const listener = Deno.listen({ port: 4505 }); - * - * console.log("server listening on http://localhost:4505"); - * - * const serve = server.serve(listener); - * setTimeout(() => { - * console.log(server.addrs); - * }, 1000); - * await serve; - * ``` - * - * @returns List of addresses. - */ - get addrs(): Deno.Addr[] { - return Array.from(this.#listeners).map((listener) => listener.addr); - } - - /** - * Responds to an HTTP request. - * - * @param requestEvent The HTTP request to respond to. - * @param connInfo Information about the underlying connection. - */ - async #respond( - requestEvent: Deno.RequestEvent, - connInfo: ConnInfo, - ) { - let response: Response; - try { - // Handle the request event, generating a response. - response = await this.#handler(requestEvent.request, connInfo); - - if (response.bodyUsed && response.body !== null) { - throw new TypeError("Response body already consumed."); - } - } catch (error: unknown) { - // Invoke onError handler when request handler throws. - response = await this.#onError(error); - } - - try { - // Send the response. - await requestEvent.respondWith(response); - } catch { - // `respondWith()` can throw for various reasons, including downstream and - // upstream connection errors, as well as errors thrown during streaming - // of the response content. In order to avoid false negatives, we ignore - // the error here and let `serveHttp` close the connection on the - // following iteration if it is in fact a downstream connection error. - } - } - - /** - * Serves all HTTP requests on a single connection. - * - * @param httpConn The HTTP connection to yield requests from. - * @param connInfo Information about the underlying connection. - */ - async #serveHttp(httpConn: Deno.HttpConn, connInfo: ConnInfo) { - while (!this.#closed) { - let requestEvent: Deno.RequestEvent | null; - - try { - // Yield the new HTTP request on the connection. - requestEvent = await httpConn.nextRequest(); - } catch { - // Connection has been closed. - break; - } - - if (requestEvent === null) { - // Connection has been closed. - break; - } - - // Respond to the request. Note we do not await this async method to - // allow the connection to handle multiple requests in the case of h2. - this.#respond(requestEvent, connInfo); - } - - this.#closeHttpConn(httpConn); - } - - /** - * Accepts all connections on a single network listener. - * - * @param listener The listener to accept connections from. - */ - async #accept(listener: Deno.Listener) { - let acceptBackoffDelay: number | undefined; - - while (!this.#closed) { - let conn: Deno.Conn; - - try { - // Wait for a new connection. - conn = await listener.accept(); - } catch (error) { - if ( - // The listener is closed. - error instanceof Deno.errors.BadResource || - // TLS handshake errors. - error instanceof Deno.errors.InvalidData || - error instanceof Deno.errors.UnexpectedEof || - error instanceof Deno.errors.ConnectionReset || - error instanceof Deno.errors.NotConnected - ) { - // Backoff after transient errors to allow time for the system to - // recover, and avoid blocking up the event loop with a continuously - // running loop. - if (!acceptBackoffDelay) { - acceptBackoffDelay = INITIAL_ACCEPT_BACKOFF_DELAY; - } else { - acceptBackoffDelay *= 2; - } - - if (acceptBackoffDelay >= MAX_ACCEPT_BACKOFF_DELAY) { - acceptBackoffDelay = MAX_ACCEPT_BACKOFF_DELAY; - } - - try { - await delay(acceptBackoffDelay, { - signal: this.#acceptBackoffDelayAbortController.signal, - }); - } catch (err: unknown) { - // The backoff delay timer is aborted when closing the server. - if (!(err instanceof DOMException && err.name === "AbortError")) { - throw err; - } - } - - continue; - } - - throw error; - } - - acceptBackoffDelay = undefined; - - // "Upgrade" the network connection into an HTTP connection. - let httpConn: Deno.HttpConn; - - try { - // deno-lint-ignore no-deprecated-deno-api - httpConn = Deno.serveHttp(conn); - } catch { - // Connection has been closed. - continue; - } - - // Closing the underlying listener will not close HTTP connections, so we - // track for closure upon server close. - this.#trackHttpConnection(httpConn); - - const connInfo: ConnInfo = { - localAddr: conn.localAddr, - remoteAddr: conn.remoteAddr, - }; - - // Serve the requests that arrive on the just-accepted connection. Note - // we do not await this async method to allow the server to accept new - // connections. - this.#serveHttp(httpConn, connInfo); - } - } - - /** - * Untracks and closes an HTTP connection. - * - * @param httpConn The HTTP connection to close. - */ - #closeHttpConn(httpConn: Deno.HttpConn) { - this.#untrackHttpConnection(httpConn); - - try { - httpConn.close(); - } catch { - // Connection has already been closed. - } - } - - /** - * Adds the listener to the internal tracking list. - * - * @param listener Listener to track. - */ - #trackListener(listener: Deno.Listener) { - this.#listeners.add(listener); - } - - /** - * Removes the listener from the internal tracking list. - * - * @param listener Listener to untrack. - */ - #untrackListener(listener: Deno.Listener) { - this.#listeners.delete(listener); - } - - /** - * Adds the HTTP connection to the internal tracking list. - * - * @param httpConn HTTP connection to track. - */ - #trackHttpConnection(httpConn: Deno.HttpConn) { - this.#httpConnections.add(httpConn); - } - - /** - * Removes the HTTP connection from the internal tracking list. - * - * @param httpConn HTTP connection to untrack. - */ - #untrackHttpConnection(httpConn: Deno.HttpConn) { - this.#httpConnections.delete(httpConn); - } -} - -/** - * Additional serve options. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeInit} instead. - */ -export interface ServeInit extends Partial { - /** An AbortSignal to close the server and all connections. */ - signal?: AbortSignal; - - /** The handler to invoke when route handlers throw an error. */ - onError?: (error: unknown) => Response | Promise; - - /** The callback which is called when the server started listening */ - onListen?: (params: { hostname: string; port: number }) => void; -} - -/** - * Additional serve listener options. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeOptions} instead. - */ -export interface ServeListenerOptions { - /** An AbortSignal to close the server and all connections. */ - signal?: AbortSignal; - - /** The handler to invoke when route handlers throw an error. */ - onError?: (error: unknown) => Response | Promise; - - /** The callback which is called when the server started listening */ - onListen?: (params: { hostname: string; port: number }) => void; -} - -/** - * Constructs a server, accepts incoming connections on the given listener, and - * handles requests on these connections with the given handler. - * - * @example Usage - * ```ts no-eval - * import { serveListener } from "@std/http/server"; - * - * const listener = Deno.listen({ port: 4505 }); - * - * console.log("server listening on http://localhost:4505"); - * - * await serveListener(listener, (request) => { - * const body = `Your user-agent is:\n\n${request.headers.get( - * "user-agent", - * ) ?? "Unknown"}`; - * - * return new Response(body, { status: 200 }); - * }); - * ``` - * - * @param listener The listener to accept connections from. - * @param handler The handler for individual HTTP requests. - * @param options Optional serve options. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.serve} instead. - */ -export async function serveListener( - listener: Deno.Listener, - handler: Handler, - options?: ServeListenerOptions, -): Promise { - const server = new Server({ handler, onError: options?.onError }); - - options?.signal?.addEventListener("abort", () => server.close(), { - once: true, - }); - - return await server.serve(listener); -} - -function hostnameForDisplay(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 - return hostname === "0.0.0.0" ? "localhost" : hostname; -} - -/** - * Serves HTTP requests with the given 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 "0.0.0.0". - * - * @example The below example serves with the port 8000. - * ```ts no-eval - * import { serve } from "@std/http/server"; - * serve((_req) => new Response("Hello, world")); - * ``` - * - * @example You can change the listening address by the `hostname` and `port` options. - * The below example serves with the port 3000. - * - * ```ts no-eval - * import { serve } from "@std/http/server"; - * serve((_req) => new Response("Hello, world"), { port: 3000 }); - * ``` - * - * @example `serve` function prints the message `Listening on http://:/` - * on start-up by default. If you like to change this message, you can specify - * `onListen` option to override it. - * - * ```ts no-eval - * import { serve } from "@std/http/server"; - * serve((_req) => new Response("Hello, world"), { - * onListen({ port, hostname }) { - * console.log(`Server started at http://${hostname}:${port}`); - * // ... more info specific to your server .. - * }, - * }); - * ``` - * - * @example You can also specify `undefined` or `null` to stop the logging behavior. - * - * ```ts no-eval - * import { serve } from "@std/http/server"; - * serve((_req) => new Response("Hello, world"), { onListen: undefined }); - * ``` - * - * @param handler The handler for individual HTTP requests. - * @param options The options. See `ServeInit` documentation for details. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.serve} instead. - */ -export async function serve( - handler: Handler, - options: ServeInit = {}, -): Promise { - let port = options.port ?? 8000; - if (typeof port !== "number") { - port = Number(port); - } - - const hostname = options.hostname ?? "0.0.0.0"; - const server = new Server({ - port, - hostname, - handler, - onError: options.onError, - }); - - options?.signal?.addEventListener("abort", () => server.close(), { - once: true, - }); - - const listener = Deno.listen({ - port, - hostname, - transport: "tcp", - }); - - const s = server.serve(listener); - - port = (server.addrs[0] as Deno.NetAddr).port; - - if ("onListen" in options) { - options.onListen?.({ port, hostname }); - } else { - console.log(`Listening on http://${hostnameForDisplay(hostname)}:${port}/`); - } - - return await s; -} - -/** - * Initialization parameters for {@linkcode serveTls}. - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.ServeTlsOptions} instead. - */ -export interface ServeTlsInit extends ServeInit { - /** Server private key in PEM format */ - key?: string; - - /** Cert chain in PEM format */ - cert?: string; - - /** The path to the file containing the TLS private key. */ - keyFile?: string; - - /** The path to the file containing the TLS certificate */ - certFile?: string; -} - -/** - * Serves HTTPS requests with the given handler. - * - * You must specify `key` or `keyFile` and `cert` or `certFile` options. - * - * You can specify an object with a port and hostname option, which is the - * address to listen on. The default is port 8443 on hostname "0.0.0.0". - * - * @example The below example serves with the default port 8443. - * - * ```ts no-eval - * import { serveTls } from "@std/http/server"; - * - * const cert = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"; - * const key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"; - * serveTls((_req) => new Response("Hello, world"), { cert, key }); - * - * // Or - * - * const certFile = "/path/to/certFile.crt"; - * const keyFile = "/path/to/keyFile.key"; - * serveTls((_req) => new Response("Hello, world"), { certFile, keyFile }); - * ``` - * - * @example `serveTls` function prints the message `Listening on https://:/` - * on start-up by default. If you like to change this message, you can specify - * `onListen` option to override it. - * - * ```ts no-eval - * import { serveTls } from "@std/http/server"; - * const certFile = "/path/to/certFile.crt"; - * const keyFile = "/path/to/keyFile.key"; - * serveTls((_req) => new Response("Hello, world"), { - * certFile, - * keyFile, - * onListen({ port, hostname }) { - * console.log(`Server started at https://${hostname}:${port}`); - * // ... more info specific to your server .. - * }, - * }); - * ``` - * - * @example You can also specify `undefined` or `null` to stop the logging behavior. - * - * ```ts no-eval - * import { serveTls } from "@std/http/server"; - * const certFile = "/path/to/certFile.crt"; - * const keyFile = "/path/to/keyFile.key"; - * serveTls((_req) => new Response("Hello, world"), { - * certFile, - * keyFile, - * onListen: undefined, - * }); - * ``` - * - * @param handler The handler for individual HTTPS requests. - * @param options The options. See `ServeTlsInit` documentation for details. - * @returns - * - * @deprecated This will be removed in 1.0.0. Use {@linkcode Deno.serve} instead. - */ -export async function serveTls( - handler: Handler, - options: ServeTlsInit, -): Promise { - if (!options.key && !options.keyFile) { - throw new Error("TLS config is given, but 'key' is missing."); - } - - if (!options.cert && !options.certFile) { - throw new Error("TLS config is given, but 'cert' is missing."); - } - - let port = options.port ?? 8443; - if (typeof port !== "number") { - port = Number(port); - } - - const hostname = options.hostname ?? "0.0.0.0"; - const server = new Server({ - port, - hostname, - handler, - onError: options.onError, - }); - - options?.signal?.addEventListener("abort", () => server.close(), { - once: true, - }); - - // deno-lint-ignore no-sync-fn-in-async-fn - const key = options.key || Deno.readTextFileSync(options.keyFile!); - // deno-lint-ignore no-sync-fn-in-async-fn - const cert = options.cert || Deno.readTextFileSync(options.certFile!); - - const listener = Deno.listenTls({ - port, - hostname, - cert, - key, - transport: "tcp", - // ALPN protocol support not yet stable. - // alpnProtocols: ["h2", "http/1.1"], - }); - - const s = server.serve(listener); - - port = (server.addrs[0] as Deno.NetAddr).port; - - if ("onListen" in options) { - options.onListen?.({ port, hostname }); - } else { - console.log( - `Listening on https://${hostnameForDisplay(hostname)}:${port}/`, - ); - } - - return await s; -} diff --git a/http/server_test.ts b/http/server_test.ts deleted file mode 100644 index be9fb374d..000000000 --- a/http/server_test.ts +++ /dev/null @@ -1,1631 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { - type ConnInfo, - serve, - serveListener, - Server, - serveTls, -} from "./server.ts"; -import { mockConn as createMockConn } from "./_mock_conn.ts"; -import { dirname, fromFileUrl, join, resolve } from "@std/path"; -import { writeAll } from "@std/io/write-all"; -import { readAll } from "@std/io/read-all"; -import { delay } from "@std/async"; -import { - assert, - assertEquals, - assertNotEquals, - assertRejects, - assertStrictEquals, - assertThrows, - unreachable, -} from "@std/assert"; - -const moduleDir = dirname(fromFileUrl(import.meta.url)); -const testdataDir = resolve(moduleDir, "testdata"); - -let port = 4800; -function getPort() { - return port++; -} - -type AcceptCallSideEffect = ({ - acceptCallCount, -}: { - acceptCallCount: number; -}) => void | Promise; - -class MockListener implements Deno.Listener { - conn: Deno.Conn; - #closed = false; - #rejectionError?: Error; - #rejectionCount: number; - #acceptCallSideEffect: AcceptCallSideEffect; - acceptCallTimes: number[] = []; - acceptCallIntervals: number[] = []; - acceptCallCount = 0; - - constructor({ - conn, - rejectionError, - rejectionCount = Infinity, - acceptCallSideEffect = () => {}, - }: { - conn: Deno.Conn; - rejectionError?: Error; - rejectionCount?: number; - acceptCallSideEffect?: AcceptCallSideEffect; - }) { - this.conn = conn; - this.#rejectionError = rejectionError; - this.#rejectionCount = rejectionCount; - this.#acceptCallSideEffect = acceptCallSideEffect; - } - - get addr(): Deno.Addr { - return this.conn.localAddr; - } - - get rid(): number { - return 4505; - } - - #shouldReject(): boolean { - return ( - typeof this.#rejectionError !== "undefined" && - this.acceptCallCount <= this.#rejectionCount - ); - } - - async accept(): Promise { - if (this.#closed) { - throw new Deno.errors.BadResource("MockListener has closed"); - } - - const now = performance.now(); - this.acceptCallIntervals.push(now - (this.acceptCallTimes.at(-1) ?? now)); - this.acceptCallTimes.push(now); - this.acceptCallCount++; - this.#acceptCallSideEffect({ acceptCallCount: this.acceptCallCount }); - - await delay(0); - - return this.#shouldReject() - ? Promise.reject(this.#rejectionError) - : Promise.resolve(this.conn); - } - - [Symbol.dispose]() { - this.close(); - } - - close() { - this.#closed = true; - } - - async *[Symbol.asyncIterator](): AsyncIterableIterator { - while (true) { - if (this.#closed) { - break; - } - - const now = performance.now(); - this.acceptCallIntervals.push(now - (this.acceptCallTimes.at(-1) ?? now)); - this.acceptCallTimes.push(now); - this.acceptCallCount++; - this.#acceptCallSideEffect({ acceptCallCount: this.acceptCallCount }); - - await delay(0); - - if (this.#shouldReject()) { - throw this.#rejectionError; - } - - yield this.conn; - } - } - - ref() { - } - - unref() { - } -} - -Deno.test( - "Server exposes the addresses the server is listening on as addrs property", - async () => { - const listenerOneOptions = { - hostname: "127.0.0.1", - port: getPort(), - }; - const listenerTwoOptions = { - hostname: "127.0.0.1", - port: getPort(), - }; - const listenerOne = Deno.listen(listenerOneOptions); - const listenerTwo = Deno.listen(listenerTwoOptions); - - const addrHostname = "0.0.0.0"; - const addrPort = getPort(); - const handler = () => new Response(); - - const server = new Server({ - port: addrPort, - hostname: addrHostname, - handler, - }); - const servePromiseOne = server.serve(listenerOne); - const servePromiseTwo = server.serve(listenerTwo); - const servePromiseThree = server.listenAndServe(); - - try { - assertEquals(server.addrs.length, 3); - assertEquals(server.addrs[0]!.transport, "tcp"); - assertEquals( - (server.addrs[0] as Deno.NetAddr).hostname, - listenerOneOptions.hostname, - ); - assertEquals( - (server.addrs[0] as Deno.NetAddr).port, - listenerOneOptions.port, - ); - assertEquals(server.addrs[1]!.transport, "tcp"); - assertEquals( - (server.addrs[1] as Deno.NetAddr).hostname, - listenerTwoOptions.hostname, - ); - assertEquals( - (server.addrs[1] as Deno.NetAddr).port, - listenerTwoOptions.port, - ); - assertEquals(server.addrs[2]!.transport, "tcp"); - assertEquals((server.addrs[2] as Deno.NetAddr).hostname, addrHostname); - assertEquals((server.addrs[2] as Deno.NetAddr).port, addrPort); - } finally { - server.close(); - await servePromiseOne; - await servePromiseTwo; - await servePromiseThree; - } - }, -); - -Deno.test("Server exposes whether the server is closed as closed property", () => { - const handler = () => new Response(); - const server = new Server({ handler }); - try { - assertEquals(server.closed, false); - } finally { - server.close(); - assertEquals(server.closed, true); - } -}); - -Deno.test( - "Server.close() throws an error if the server is already closed", - () => { - const handler = () => new Response(); - const server = new Server({ handler }); - server.close(); - - assertThrows(() => server.close(), Deno.errors.Http, "Server closed"); - }, -); - -Deno.test( - "Server.serve() throws an error if the server is already closed", - async () => { - const handler = () => new Response(); - const server = new Server({ handler }); - server.close(); - - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - using listener = Deno.listen(listenOptions); - - await assertRejects( - () => server.serve(listener), - Deno.errors.Http, - "Server closed", - ); - }, -); - -Deno.test( - "Server.listenAndServe() throws an error if the server is already closed", - async () => { - const handler = () => new Response(); - const server = new Server({ handler }); - server.close(); - - await assertRejects( - () => server.listenAndServe(), - Deno.errors.Http, - "Server closed", - ); - }, -); - -Deno.test( - "Server.listenAndServeTls() throws an error if the server is already closed", - async () => { - const handler = () => new Response(); - const server = new Server({ handler }); - server.close(); - - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - - await assertRejects( - () => server.listenAndServeTls(certFile, keyFile), - Deno.errors.Http, - "Server closed", - ); - }, -); - -Deno.test( - "serveListener() does not overwrite an abort signal handler", - async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - const handler = () => new Response(); - const onAbort = () => {}; - const abortController = new AbortController(); - - abortController.signal.onabort = onAbort; - - const servePromise = serveListener(listener, handler, { - signal: abortController.signal, - }); - - try { - assertStrictEquals(abortController.signal.onabort, onAbort); - } finally { - abortController.abort(); - await servePromise; - } - }, -); - -Deno.test( - "serve() does not overwrite an abort signal handler", - async () => { - const handler = () => new Response(); - const onAbort = () => {}; - const abortController = new AbortController(); - - abortController.signal.onabort = onAbort; - - const servePromise = serve(handler, { - hostname: "localhost", - port: getPort(), - signal: abortController.signal, - }); - - try { - assertStrictEquals(abortController.signal.onabort, onAbort); - } finally { - abortController.abort(); - await servePromise; - } - }, -); - -Deno.test( - "serveTls() not overwrite an abort signal handler", - async () => { - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const handler = () => new Response(); - const onAbort = () => {}; - const abortController = new AbortController(); - - abortController.signal.onabort = onAbort; - - const servePromise = serveTls(handler, { - hostname: "localhost", - port: getPort(), - certFile, - keyFile, - signal: abortController.signal, - }); - - try { - assertStrictEquals(abortController.signal.onabort, onAbort); - } finally { - abortController.abort(); - await servePromise; - } - }, -); - -Deno.test( - "serveListener() does not throw if abort when the server is already closed", - async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - const handler = () => new Response(); - const abortController = new AbortController(); - - const servePromise = serveListener(listener, handler, { - signal: abortController.signal, - }); - - abortController.abort(); - - try { - abortController.abort(); - } finally { - await servePromise; - } - }, -); - -Deno.test( - "serve() does not throw if abort when the server is already closed", - async () => { - const handler = () => new Response(); - const abortController = new AbortController(); - - const servePromise = serve(handler, { - hostname: "localhost", - port: getPort(), - signal: abortController.signal, - }); - - abortController.abort(); - - try { - abortController.abort(); - } finally { - await servePromise; - } - }, -); - -Deno.test( - "serveTls() does not throw if abort when the server is already closed", - async () => { - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const handler = () => new Response(); - const abortController = new AbortController(); - - const servePromise = serveTls(handler, { - hostname: "localhost", - port: getPort(), - certFile, - keyFile, - signal: abortController.signal, - }); - - abortController.abort(); - - try { - abortController.abort(); - } finally { - await servePromise; - } - }, -); - -Deno.test(`Server.serve() responds with internal server error if response body is already consumed`, async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const url = `http://${listenOptions.hostname}:${listenOptions.port}`; - const body = "Internal Server Error"; - const status = 500; - - async function handler() { - const response = new Response("Hello, world!"); - await response.text(); - return response; - } - - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - try { - const response = await fetch(url); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - server.close(); - await servePromise; - } -}); - -Deno.test(`Server.serve() handles requests`, async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const url = `http://${listenOptions.hostname}:${listenOptions.port}`; - const status = 418; - const method = "GET"; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - server.close(); - await servePromise; - } -}); - -Deno.test(`Server.listenAndServe() handles requests`, async () => { - const hostname = "localhost"; - const port = getPort(); - const url = `http://${hostname}:${port}`; - const status = 418; - const method = "POST"; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - - const server = new Server({ hostname, port, handler }); - const servePromise = server.listenAndServe(); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - server.close(); - await servePromise; - } -}); - -Deno.test({ - // PermissionDenied: Permission denied (os error 13) - // Will pass if run as root user. - ignore: true, - name: `Server.listenAndServe() handles requests on the default HTTP port`, - fn: async () => { - const addr = "localhost"; - const url = `http://${addr}`; - const status = 418; - const method = "PATCH"; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - - const server = new Server({ hostname: addr, handler }); - const servePromise = server.listenAndServe(); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - server.close(); - await servePromise; - } - }, -}); - -Deno.test(`Server.listenAndServeTls() handles requests`, async () => { - const hostname = "localhost"; - const port = getPort(); - const addr = `${hostname}:${port}`; - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const url = `http://${addr}`; - const status = 418; - const method = "DELETE"; - const body = `${method}: ${url} - Hello Deno on HTTPS!`; - - const handler = () => new Response(body, { status }); - - const server = new Server({ hostname, port, handler }); - const servePromise = server.listenAndServeTls(certFile, keyFile); - - try { - // Invalid certificate, connection should throw on first read or write - // but should not crash the server. - using badConn = await Deno.connectTls({ - hostname, - port, - // missing certFile - }); - - await assertRejects( - () => badConn.read(new Uint8Array(1)), - Deno.errors.InvalidData, - "invalid peer certificate: UnknownIssuer", - "Read with missing certFile didn't throw an InvalidData error when it should have.", - ); - - // Valid request after invalid - using conn = await Deno.connectTls({ - hostname, - port, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await writeAll( - conn, - new TextEncoder().encode(`${method.toUpperCase()} / HTTP/1.0\r\n\r\n`), - ); - - const response = new TextDecoder().decode(await readAll(conn)); - - assert(response.includes(`HTTP/1.0 ${status}`), "Status code not correct"); - assert(response.includes(body), "Response body not correct"); - } finally { - server.close(); - await servePromise; - } -}); - -Deno.test({ - // PermissionDenied: Permission denied (os error 13) - // Will pass if run as root user. - ignore: true, - name: `Server.listenAndServeTls() handles requests on the default HTTPS port`, - fn: async () => { - const hostname = "localhost"; - const port = 443; - const addr = hostname; - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const url = `http://${addr}`; - const status = 418; - const method = "PUT"; - const body = `${method}: ${url} - Hello Deno on HTTPS!`; - - const handler = () => new Response(body, { status }); - - const server = new Server({ hostname, port, handler }); - const servePromise = server.listenAndServeTls(certFile, keyFile); - - try { - // Invalid certificate, connection should throw on first read or write - // but should not crash the server. - using badConn = await Deno.connectTls({ - hostname, - port, - // missing certFile - }); - - await assertRejects( - () => badConn.read(new Uint8Array(1)), - Deno.errors.InvalidData, - "invalid peer certificate contents: invalid peer certificate: UnknownIssuer", - "Read with missing certFile didn't throw an InvalidData error when it should have.", - ); - - // Valid request after invalid - using conn = await Deno.connectTls({ - hostname, - port, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await writeAll( - conn, - new TextEncoder().encode(`${method.toUpperCase()} / HTTP/1.0\r\n\r\n`), - ); - - const response = new TextDecoder().decode(await readAll(conn)); - - assert( - response.includes(`HTTP/1.0 ${status}`), - "Status code not correct", - ); - assert(response.includes(body), "Response body not correct"); - } finally { - server.close(); - await servePromise; - } - }, -}); - -Deno.test(`serve() handles requests`, async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const url = `http://${listenOptions.hostname}:${listenOptions.port}`; - const status = 418; - const method = "GET"; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - const abortController = new AbortController(); - - const servePromise = serveListener(listener, handler, { - signal: abortController.signal, - }); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test(`serve() handles requests`, async () => { - const hostname = "localhost"; - const port = getPort(); - const url = `http://${hostname}:${port}`; - const status = 418; - const method = "POST"; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - const abortController = new AbortController(); - - const servePromise = serve(handler, { - hostname, - port, - signal: abortController.signal, - }); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test(`serve() listens on the port 8000 by default`, async () => { - const url = "http://localhost:8000"; - const body = "Hello from port 8000"; - - const handler = () => new Response(body); - const abortController = new AbortController(); - - const servePromise = serve(handler, { - signal: abortController.signal, - }); - servePromise.catch(() => {}); - - try { - const response = await fetch(url); - assertEquals(await response.text(), body); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test(`serve() handles websocket requests`, async () => { - const hostname = "localhost"; - const port = getPort(); - const url = `ws://${hostname}:${port}`; - const message = `${url} - Hello Deno on WebSocket!`; - - const abortController = new AbortController(); - - const servePromise = serve( - (request) => { - const { socket, response } = Deno.upgradeWebSocket(request); - // Return the received message as it is - socket.onmessage = (event) => socket.send(event.data); - return response; - }, - { - hostname, - port, - signal: abortController.signal, - }, - ); - - const ws = new WebSocket(url); - const closePromise = new Promise((resolve) => { - ws.onclose = resolve; - }); - try { - ws.onopen = () => ws.send(message); - const response = await new Promise((resolve) => { - ws.onmessage = (event) => resolve(event.data); - }); - assertEquals(response, message); - } finally { - ws.close(); - abortController.abort(); - await servePromise; - await closePromise; - } -}); - -Deno.test(`Server.listenAndServeTls() handles requests`, async () => { - const hostname = "localhost"; - const port = getPort(); - const addr = `${hostname}:${port}`; - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const url = `http://${addr}`; - const status = 418; - const method = "PATCH"; - const body = `${method}: ${url} - Hello Deno on HTTPS!`; - - const handler = () => new Response(body, { status }); - const abortController = new AbortController(); - - const servePromise = serveTls(handler, { - hostname, - port, - certFile, - keyFile, - signal: abortController.signal, - }); - - try { - // Invalid certificate, connection should throw on first read or write - // but should not crash the server. - using badConn = await Deno.connectTls({ - hostname, - port, - // missing certFile - }); - - await assertRejects( - () => badConn.read(new Uint8Array(1)), - Deno.errors.InvalidData, - "invalid peer certificate: UnknownIssuer", - "Read with missing certFile didn't throw an InvalidData error when it should have.", - ); - - // Valid request after invalid - using conn = await Deno.connectTls({ - hostname, - port, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await writeAll( - conn, - new TextEncoder().encode(`${method.toUpperCase()} / HTTP/1.0\r\n\r\n`), - ); - - const response = new TextDecoder().decode(await readAll(conn)); - - assert(response.includes(`HTTP/1.0 ${status}`), "Status code not correct"); - assert(response.includes(body), "Response body not correct"); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test( - "Server does not reject when the listener is closed (though the server will continually try and fail to accept connections on the listener until it is closed)", - async () => { - using listener = Deno.listen({ port: getPort() }); - const handler = () => new Response(); - const server = new Server({ handler }); - - let servePromise; - - try { - servePromise = server.serve(listener); - await delay(10); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test( - "Server does not reject when there is a tls handshake with tcp corruption", - async () => { - const conn = createMockConn(); - const rejectionError = new Deno.errors.InvalidData( - "test-tcp-corruption-error", - ); - const listener = new MockListener({ conn, rejectionError }); - const handler = () => new Response(); - const server = new Server({ handler }); - - let servePromise; - - try { - servePromise = server.serve(listener); - await delay(10); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test( - "Server does not reject when the tls session is aborted", - async () => { - const conn = createMockConn(); - const rejectionError = new Deno.errors.ConnectionReset( - "test-tls-session-aborted-error", - ); - const listener = new MockListener({ conn, rejectionError }); - const handler = () => new Response(); - const server = new Server({ handler }); - - let servePromise; - - try { - servePromise = server.serve(listener); - await delay(10); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test("Server does not reject when the socket is closed", async () => { - const conn = createMockConn(); - const rejectionError = new Deno.errors.NotConnected( - "test-socket-closed-error", - ); - const listener = new MockListener({ conn, rejectionError }); - const handler = () => new Response(); - const server = new Server({ handler }); - - let servePromise; - - try { - servePromise = server.serve(listener); - await delay(10); - } finally { - server.close(); - await servePromise; - } -}); - -Deno.test( - "Server does implement a backoff delay when accepting a connection throws an expected error and reset the backoff when successfully accepting a connection again", - async () => { - // acceptDelay(n) = 5 * 2^n for n=0...7 capped at 1000 afterwards. - const expectedBackoffDelays = [ - 5, - 10, - 20, - 40, - 80, - 160, - 320, - 640, - 1000, - 1000, - ]; - const rejectionCount = expectedBackoffDelays.length; - - let resolver: (value: unknown) => void; - - // Construct a promise we know will only resolve after listener.accept() has - // been called enough times to assert on our expected backoff delays, i.e. - // the number of rejections + 1 success. - const expectedBackoffDelaysCompletedPromise = new Promise((resolve) => { - resolver = resolve; - }); - - const acceptCallSideEffect = ({ - acceptCallCount, - }: { - acceptCallCount: number; - }) => { - if (acceptCallCount > rejectionCount + 1) { - resolver(undefined); - } - }; - - const conn = createMockConn(); - const rejectionError = new Deno.errors.NotConnected( - "test-socket-closed-error", - ); - - const listener = new MockListener({ - conn, - rejectionError, - rejectionCount, - acceptCallSideEffect, - }); - - const handler = () => new Response(); - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - // Wait for all the expected failures / backoff periods to have completed. - await expectedBackoffDelaysCompletedPromise; - - server.close(); - await servePromise; - - listener.acceptCallIntervals.shift(); - console.log("\n Accept call intervals vs expected backoff intervals:"); - console.table( - listener.acceptCallIntervals.map((col, i) => [ - col, - expectedBackoffDelays[i] ?? "<1000, reset", - ]), - ); - - // Assert that the time between the accept calls is greater than or equal to - // the expected backoff delay. - for (let i = 0; i < rejectionCount; i++) { - assertEquals( - listener.acceptCallIntervals[i]! >= expectedBackoffDelays[i]!, - true, - ); - } - - // Assert that the backoff delay has been reset following successfully - // accepting a connection, i.e. it doesn't remain at 1000ms. - assertEquals(listener.acceptCallIntervals[rejectionCount]! < 1000, true); - }, -); - -Deno.test("Server does not leak async ops when closed", () => { - const hostname = "127.0.0.1"; - const port = getPort(); - const handler = () => new Response(); - const server = new Server({ port, hostname, handler }); - server.listenAndServe(); - server.close(); - // Otherwise, the test would fail with: AssertionError: Test case is leaking async ops. -}); - -Deno.test("Server aborts accept backoff delay when closing", async () => { - const hostname = "127.0.0.1"; - const port = getPort(); - const handler = () => new Response(); - - const rejectionError = new Deno.errors.NotConnected( - "test-socket-closed-error", - ); - const rejectionCount = 1; - const conn = createMockConn(); - - const listener = new MockListener({ - conn, - rejectionError, - rejectionCount, - }); - - const server = new Server({ port, hostname, handler }); - server.serve(listener); - - // Wait until the connection is rejected and the backoff delay starts. - await delay(0); - - // Close the server, this should end the test without still having an active timer that would trigger an - // AssertionError: Test case is leaking async ops. - server.close(); -}); - -Deno.test("Server rejects if the listener throws an unexpected error accepting a connection", async () => { - const conn = createMockConn(); - const rejectionError = new Error("test-unexpected-error"); - const listener = new MockListener({ conn, rejectionError }); - const handler = () => new Response(); - const server = new Server({ handler }); - await assertRejects( - () => server.serve(listener), - Error, - rejectionError.message, - ); -}); - -Deno.test( - "Server rejects if the listener throws an unexpected error accepting a connection", - async () => { - const conn = createMockConn(); - const rejectionError = new Error("test-unexpected-error"); - const listener = new MockListener({ conn, rejectionError }); - const handler = () => new Response(); - const server = new Server({ handler }); - await assertRejects( - () => server.serve(listener), - Error, - rejectionError.message, - ); - }, -); - -Deno.test( - "Server does not reject when the connection is closed before the message is complete", - async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const onRequest = Promise.withResolvers(); - const postRespondWith = Promise.withResolvers(); - - const handler = async () => { - onRequest.resolve(); - - await delay(0); - - try { - return new Response("test-response"); - } finally { - postRespondWith.resolve(); - } - }; - - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - using conn = await Deno.connect(listenOptions); - - await writeAll(conn, new TextEncoder().encode(`GET / HTTP/1.0\r\n\r\n`)); - - await onRequest.promise; - - await postRespondWith.promise; - server.close(); - - await servePromise; - }, -); - -Deno.test("Server does not reject when the handler throws", async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const postRespondWith = Promise.withResolvers(); - - const handler = () => { - try { - throw new Error("test-error"); - } finally { - postRespondWith.resolve(); - } - }; - - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - using conn = await Deno.connect(listenOptions); - - await writeAll(conn, new TextEncoder().encode(`GET / HTTP/1.0\r\n\r\n`)); - - await postRespondWith.promise; - server.close(); - await servePromise; -}); - -Deno.test("Server does not close the http2 downstream connection when the response stream throws", async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - cert: await Deno.readTextFile(join(testdataDir, "tls/localhost.crt")), - key: await Deno.readTextFile(join(testdataDir, "tls/localhost.key")), - alpnProtocols: ["h2"], - }; - const listener = Deno.listenTls(listenOptions); - const url = `https://${listenOptions.hostname}:${listenOptions.port}/`; - - let n = 0; - const a = Promise.withResolvers(); - const connections = new Set(); - - const handler = (_req: Request, connInfo: ConnInfo) => { - connections.add(connInfo); - return new Response( - new ReadableStream({ - async start(controller) { - n++; - if (n === 3) { - throw new Error("test-error"); - } - await a.promise; - controller.enqueue(new TextEncoder().encode("a")); - controller.close(); - }, - }), - ); - }; - - const server = new Server({ handler }); - const servePromise = server.serve(listener); - - const caCert = await Deno.readTextFile( - join(testdataDir, "tls/RootCA.pem"), - ); - const client = Deno.createHttpClient({ - caCerts: [caCert], - }); - const resp1 = await fetch(url, { client }); - const resp2 = await fetch(url, { client }); - - const err = await assertRejects(async () => { - const resp3 = await fetch(url, { client }); - const _data = await resp3.text(); - }); - assert(err); - a.resolve(); - assertEquals(await resp1.text(), "a"); - assertEquals(await resp2.text(), "a"); - - const numConns = connections.size; - assertEquals( - numConns, - 1, - `fetch should have reused a single connection, but used ${numConns} instead.`, - ); - assertEquals(n, 3, "The handler should have been called three times"); - - client.close(); - server.close(); - await servePromise; -}); - -Deno.test("Server parses IPV6 addresses", async () => { - const hostname = "[::1]"; - const port = getPort(); - const url = `http://${hostname}:${port}`; - const method = "GET"; - const status = 418; - const body = `${method}: ${url} - Hello Deno on HTTP!`; - - const handler = () => new Response(body, { status }); - const abortController = new AbortController(); - - const servePromise = serve(handler, { - hostname, - port, - signal: abortController.signal, - }); - - try { - const response = await fetch(url, { method }); - assertEquals(await response.text(), body); - assertEquals(response.status, status); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test("Server.serve() can be called multiple times", async () => { - const listenerOneOptions = { - hostname: "localhost", - port: getPort(), - }; - const listenerTwoOptions = { - hostname: "localhost", - port: getPort(), - }; - const listenerOne = Deno.listen(listenerOneOptions); - const listenerTwo = Deno.listen(listenerTwoOptions); - - const handler = (_request: Request, connInfo: ConnInfo) => { - if ((connInfo.localAddr as Deno.NetAddr).port === listenerOneOptions.port) { - return new Response("Hello listener one!"); - } else if ( - (connInfo.localAddr as Deno.NetAddr).port === listenerTwoOptions.port - ) { - return new Response("Hello listener two!"); - } - - unreachable(); - }; - - const server = new Server({ handler }); - const servePromiseOne = server.serve(listenerOne); - const servePromiseTwo = server.serve(listenerTwo); - - try { - const responseOne = await fetch( - `http://${listenerOneOptions.hostname}:${listenerOneOptions.port}`, - ); - assertEquals(await responseOne.text(), "Hello listener one!"); - - const responseTwo = await fetch( - `http://${listenerTwoOptions.hostname}:${listenerTwoOptions.port}`, - ); - assertEquals(await responseTwo.text(), "Hello listener two!"); - } finally { - server.close(); - await servePromiseOne; - await servePromiseTwo; - } -}); - -Deno.test( - "Server.listenAndServe() throws if called multiple times", - async () => { - const handler = () => unreachable(); - - const server = new Server({ port: 4505, handler }); - const servePromise = server.listenAndServe(); - - try { - await assertRejects(() => server.listenAndServe(), Deno.errors.AddrInUse); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test( - "Server.listenAndServeTls() throws if called multiple times", - async () => { - const handler = () => unreachable(); - - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - - const server = new Server({ port: 4505, handler }); - const servePromise = server.listenAndServeTls(certFile, keyFile); - - try { - await assertRejects( - () => server.listenAndServeTls(certFile, keyFile), - Deno.errors.AddrInUse, - ); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test( - "Sever() handler is called with the request instance and connection information", - async () => { - const hostname = "127.0.0.1"; - const port = getPort(); - const addr = `${hostname}:${port}`; - - let receivedRequest: Request; - let receivedConnInfo: ConnInfo; - - const handler = (request: Request, connInfo: ConnInfo) => { - receivedRequest = request; - receivedConnInfo = connInfo; - - return new Response("Hello Deno!"); - }; - - const server = new Server({ hostname, port, handler }); - const servePromise = server.listenAndServe(); - - const url = `http://${addr}/`; - - try { - const response = await fetch(url); - await response.text(); - - assertEquals(receivedRequest!.url, url); - assertEquals(receivedConnInfo!.localAddr.transport, "tcp"); - assertEquals( - (receivedConnInfo!.localAddr as Deno.NetAddr).hostname, - hostname, - ); - assertEquals((receivedConnInfo!.localAddr as Deno.NetAddr).port, port); - assertEquals(receivedConnInfo!.remoteAddr.transport, "tcp"); - assertEquals( - (receivedConnInfo!.remoteAddr as Deno.NetAddr).hostname, - hostname, - ); - } finally { - server.close(); - await servePromise; - } - }, -); - -Deno.test("serve() calls onError when Handler throws", async () => { - const hostname = "localhost"; - const port = getPort(); - const url = `http://${hostname}:${port}`; - const handler = (_request: Request, _connInfo: ConnInfo) => { - throw new Error("I failed to serve the request"); - }; - const abortController = new AbortController(); - - const servePromise = serve(handler, { - hostname, - port, - signal: abortController.signal, - }); - - try { - const response = await fetch(url); - assertEquals(await response.text(), "Internal Server Error"); - assertEquals(response.status, 500); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test("serve() calls custom onError when Handler throws", async () => { - const hostname = "localhost"; - const port = getPort(); - const url = `http://${hostname}:${port}`; - const handler = (_request: Request, _connInfo: ConnInfo) => { - throw new Error("I failed to serve the request"); - }; - const onError = (_error: unknown) => { - return new Response("custom error page", { status: 500 }); - }; - const abortController = new AbortController(); - - const servePromise = serve(handler, { - hostname, - port, - onError, - signal: abortController.signal, - }); - - try { - const response = await fetch(url); - assertEquals(await response.text(), "custom error page"); - assertEquals(response.status, 500); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test("serveListener() calls custom onError when Handler throws", async () => { - const listenOptions = { - hostname: "localhost", - port: getPort(), - }; - const listener = Deno.listen(listenOptions); - - const url = `http://${listenOptions.hostname}:${listenOptions.port}`; - const handler = (_request: Request, _connInfo: ConnInfo) => { - throw new Error("I failed to serve the request"); - }; - const onError = (_error: unknown) => { - return new Response("custom error page", { status: 500 }); - }; - const abortController = new AbortController(); - - const servePromise = serveListener(listener, handler, { - onError, - signal: abortController.signal, - }); - - try { - const response = await fetch(url); - assertEquals(await response.text(), "custom error page"); - assertEquals(response.status, 500); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test("Server.serveTls() supports custom onError", async () => { - const hostname = "localhost"; - const port = getPort(); - const certFile = join(testdataDir, "tls/localhost.crt"); - const keyFile = join(testdataDir, "tls/localhost.key"); - const status = 500; - const method = "PATCH"; - const body = "custom error page"; - - const handler = () => { - throw new Error("I failed to serve the request."); - }; - const onError = (_error: unknown) => new Response(body, { status }); - const abortController = new AbortController(); - - const servePromise = serveTls(handler, { - hostname, - port, - certFile, - keyFile, - onError, - signal: abortController.signal, - }); - - try { - using conn = await Deno.connectTls({ - hostname, - port, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await writeAll( - conn, - new TextEncoder().encode( - `${method.toUpperCase()} / HTTP/1.0\r\n\r\n`, - ), - ); - - const response = new TextDecoder().decode(await readAll(conn)); - - assert( - response.includes(`HTTP/1.0 ${status}`), - "Status code not correct", - ); - assert( - response.includes(body), - "Response body not correct", - ); - } finally { - abortController.abort(); - await servePromise; - } -}); - -Deno.test("serve() calls onListen callback when the server started listening", () => { - const abortController = new AbortController(); - return serve((_) => new Response("hello"), { - async onListen({ hostname, port }) { - const responseText = await (await fetch("http://localhost:8000/")).text(); - assertEquals(hostname, "0.0.0.0"); - assertEquals(port, 8000); - assertEquals(responseText, "hello"); - abortController.abort(); - }, - signal: abortController.signal, - }); -}); - -Deno.test("serve() calls onListen callback with ephemeral port", () => { - const abortController = new AbortController(); - return serve((_) => new Response("hello"), { - port: 0, - async onListen({ hostname, port }) { - assertEquals(hostname, "0.0.0.0"); - assertNotEquals(port, 0); - const responseText = await (await fetch(`http://localhost:${port}/`)) - .text(); - assertEquals(responseText, "hello"); - abortController.abort(); - }, - signal: abortController.signal, - }); -}); - -Deno.test("serve() doesn't print the message when onListen set to undefined", async () => { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "eval", - "--no-lock", - ` - import { serve } from "./http/server.ts"; - serve(() => new Response("hello"), { onListen: undefined }); - Deno.exit(0); - `, - ], - }); - const { code, stdout } = await command.output(); - assertEquals(code, 0); - assertEquals(new TextDecoder().decode(stdout), ""); -}); - -Deno.test("serve() can print customized start-up message in onListen handler", async () => { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "eval", - "--no-lock", - ` - import { serve } from "./http/server.ts"; - serve(() => new Response("hello"), { onListen({ port, hostname }) { - console.log("Server started at " + hostname + " port " + port); - } }); - Deno.exit(0); - `, - ], - }); - const { stdout, code } = await command.output(); - assertEquals(code, 0); - assertEquals( - new TextDecoder().decode(stdout), - "Server started at 0.0.0.0 port 8000\n", - ); -}); - -Deno.test("serveTls() calls onListen callback with ephemeral port", () => { - const abortController = new AbortController(); - return serveTls((_) => new Response("hello"), { - port: 0, - certFile: join(testdataDir, "tls/localhost.crt"), - keyFile: join(testdataDir, "tls/localhost.key"), - async onListen({ hostname, port }) { - assertEquals(hostname, "0.0.0.0"); - assertNotEquals(port, 0); - const caCert = await Deno.readTextFile( - join(testdataDir, "tls/RootCA.pem"), - ); - const client = Deno.createHttpClient({ caCerts: [caCert] }); - const responseText = - await (await fetch(`https://localhost:${port}/`, { client })) - .text(); - client.close(); - assertEquals(responseText, "hello"); - abortController.abort(); - }, - signal: abortController.signal, - }); -}); - -Deno.test("serveTls() handles cert key injection directly from memory rather than file system.", () => { - const abortController = new AbortController(); - return serveTls((_) => new Response("hello"), { - port: 0, - cert: Deno.readTextFileSync(join(testdataDir, "tls/localhost.crt")), - key: Deno.readTextFileSync(join(testdataDir, "tls/localhost.key")), - async onListen({ hostname, port }) { - assertEquals(hostname, "0.0.0.0"); - assertNotEquals(port, 0); - const caCert = await Deno.readTextFile( - join(testdataDir, "tls/RootCA.pem"), - ); - const client = Deno.createHttpClient({ caCerts: [caCert] }); - const responseText = await ( - await fetch(`https://localhost:${port}/`, { client }) - ).text(); - client.close(); - assertEquals(responseText, "hello"); - abortController.abort(); - }, - signal: abortController.signal, - }); -}); - -Deno.test("serve() doesn't throw with string port number", () => { - const ac = new AbortController(); - return serve((_) => new Response("hello"), { - // deno-lint-ignore no-explicit-any - port: "0" as any, - onListen() { - ac.abort(); - }, - signal: ac.signal, - }); -}); - -Deno.test("serveTls() doesn't throw with string port number", () => { - const ac = new AbortController(); - return serveTls((_) => new Response("hello"), { - // deno-lint-ignore no-explicit-any - port: "0" as any, - cert: Deno.readTextFileSync(join(testdataDir, "tls/localhost.crt")), - key: Deno.readTextFileSync(join(testdataDir, "tls/localhost.key")), - onListen() { - ac.abort(); - }, - signal: ac.signal, - }); -});