summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/http.ts
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/http.ts')
-rw-r--r--ext/node/polyfills/http.ts940
1 files changed, 940 insertions, 0 deletions
diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts
new file mode 100644
index 000000000..4bfc1e1d3
--- /dev/null
+++ b/ext/node/polyfills/http.ts
@@ -0,0 +1,940 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import { TextEncoder } from "internal:deno_web/08_text_encoding.js";
+import {
+ type Deferred,
+ deferred,
+} from "internal:deno_node/polyfills/_util/async.ts";
+import {
+ _normalizeArgs,
+ ListenOptions,
+ Socket,
+} from "internal:deno_node/polyfills/net.ts";
+import { Buffer } from "internal:deno_node/polyfills/buffer.ts";
+import { ERR_SERVER_NOT_RUNNING } from "internal:deno_node/polyfills/internal/errors.ts";
+import { EventEmitter } from "internal:deno_node/polyfills/events.ts";
+import { nextTick } from "internal:deno_node/polyfills/_next_tick.ts";
+import { validatePort } from "internal:deno_node/polyfills/internal/validators.mjs";
+import {
+ Readable as NodeReadable,
+ Writable as NodeWritable,
+} from "internal:deno_node/polyfills/stream.ts";
+import { OutgoingMessage } from "internal:deno_node/polyfills/_http_outgoing.ts";
+import { Agent } from "internal:deno_node/polyfills/_http_agent.mjs";
+import { chunkExpression as RE_TE_CHUNKED } from "internal:deno_node/polyfills/_http_common.ts";
+import { urlToHttpOptions } from "internal:deno_node/polyfills/internal/url.ts";
+import {
+ constants,
+ TCP,
+} from "internal:deno_node/polyfills/internal_binding/tcp_wrap.ts";
+
+enum STATUS_CODES {
+ /** RFC 7231, 6.2.1 */
+ Continue = 100,
+ /** RFC 7231, 6.2.2 */
+ SwitchingProtocols = 101,
+ /** RFC 2518, 10.1 */
+ Processing = 102,
+ /** RFC 8297 **/
+ EarlyHints = 103,
+
+ /** RFC 7231, 6.3.1 */
+ OK = 200,
+ /** RFC 7231, 6.3.2 */
+ Created = 201,
+ /** RFC 7231, 6.3.3 */
+ Accepted = 202,
+ /** RFC 7231, 6.3.4 */
+ NonAuthoritativeInfo = 203,
+ /** RFC 7231, 6.3.5 */
+ NoContent = 204,
+ /** RFC 7231, 6.3.6 */
+ ResetContent = 205,
+ /** RFC 7233, 4.1 */
+ PartialContent = 206,
+ /** RFC 4918, 11.1 */
+ MultiStatus = 207,
+ /** RFC 5842, 7.1 */
+ AlreadyReported = 208,
+ /** RFC 3229, 10.4.1 */
+ IMUsed = 226,
+
+ /** RFC 7231, 6.4.1 */
+ MultipleChoices = 300,
+ /** RFC 7231, 6.4.2 */
+ MovedPermanently = 301,
+ /** RFC 7231, 6.4.3 */
+ Found = 302,
+ /** RFC 7231, 6.4.4 */
+ SeeOther = 303,
+ /** RFC 7232, 4.1 */
+ NotModified = 304,
+ /** RFC 7231, 6.4.5 */
+ UseProxy = 305,
+ /** RFC 7231, 6.4.7 */
+ TemporaryRedirect = 307,
+ /** RFC 7538, 3 */
+ PermanentRedirect = 308,
+
+ /** RFC 7231, 6.5.1 */
+ BadRequest = 400,
+ /** RFC 7235, 3.1 */
+ Unauthorized = 401,
+ /** RFC 7231, 6.5.2 */
+ PaymentRequired = 402,
+ /** RFC 7231, 6.5.3 */
+ Forbidden = 403,
+ /** RFC 7231, 6.5.4 */
+ NotFound = 404,
+ /** RFC 7231, 6.5.5 */
+ MethodNotAllowed = 405,
+ /** RFC 7231, 6.5.6 */
+ NotAcceptable = 406,
+ /** RFC 7235, 3.2 */
+ ProxyAuthRequired = 407,
+ /** RFC 7231, 6.5.7 */
+ RequestTimeout = 408,
+ /** RFC 7231, 6.5.8 */
+ Conflict = 409,
+ /** RFC 7231, 6.5.9 */
+ Gone = 410,
+ /** RFC 7231, 6.5.10 */
+ LengthRequired = 411,
+ /** RFC 7232, 4.2 */
+ PreconditionFailed = 412,
+ /** RFC 7231, 6.5.11 */
+ RequestEntityTooLarge = 413,
+ /** RFC 7231, 6.5.12 */
+ RequestURITooLong = 414,
+ /** RFC 7231, 6.5.13 */
+ UnsupportedMediaType = 415,
+ /** RFC 7233, 4.4 */
+ RequestedRangeNotSatisfiable = 416,
+ /** RFC 7231, 6.5.14 */
+ ExpectationFailed = 417,
+ /** RFC 7168, 2.3.3 */
+ Teapot = 418,
+ /** RFC 7540, 9.1.2 */
+ MisdirectedRequest = 421,
+ /** RFC 4918, 11.2 */
+ UnprocessableEntity = 422,
+ /** RFC 4918, 11.3 */
+ Locked = 423,
+ /** RFC 4918, 11.4 */
+ FailedDependency = 424,
+ /** RFC 8470, 5.2 */
+ TooEarly = 425,
+ /** RFC 7231, 6.5.15 */
+ UpgradeRequired = 426,
+ /** RFC 6585, 3 */
+ PreconditionRequired = 428,
+ /** RFC 6585, 4 */
+ TooManyRequests = 429,
+ /** RFC 6585, 5 */
+ RequestHeaderFieldsTooLarge = 431,
+ /** RFC 7725, 3 */
+ UnavailableForLegalReasons = 451,
+
+ /** RFC 7231, 6.6.1 */
+ InternalServerError = 500,
+ /** RFC 7231, 6.6.2 */
+ NotImplemented = 501,
+ /** RFC 7231, 6.6.3 */
+ BadGateway = 502,
+ /** RFC 7231, 6.6.4 */
+ ServiceUnavailable = 503,
+ /** RFC 7231, 6.6.5 */
+ GatewayTimeout = 504,
+ /** RFC 7231, 6.6.6 */
+ HTTPVersionNotSupported = 505,
+ /** RFC 2295, 8.1 */
+ VariantAlsoNegotiates = 506,
+ /** RFC 4918, 11.5 */
+ InsufficientStorage = 507,
+ /** RFC 5842, 7.2 */
+ LoopDetected = 508,
+ /** RFC 2774, 7 */
+ NotExtended = 510,
+ /** RFC 6585, 6 */
+ NetworkAuthenticationRequired = 511,
+}
+
+const METHODS = [
+ "ACL",
+ "BIND",
+ "CHECKOUT",
+ "CONNECT",
+ "COPY",
+ "DELETE",
+ "GET",
+ "HEAD",
+ "LINK",
+ "LOCK",
+ "M-SEARCH",
+ "MERGE",
+ "MKACTIVITY",
+ "MKCALENDAR",
+ "MKCOL",
+ "MOVE",
+ "NOTIFY",
+ "OPTIONS",
+ "PATCH",
+ "POST",
+ "PROPFIND",
+ "PROPPATCH",
+ "PURGE",
+ "PUT",
+ "REBIND",
+ "REPORT",
+ "SEARCH",
+ "SOURCE",
+ "SUBSCRIBE",
+ "TRACE",
+ "UNBIND",
+ "UNLINK",
+ "UNLOCK",
+ "UNSUBSCRIBE",
+];
+
+type Chunk = string | Buffer | Uint8Array;
+
+// @ts-ignore Deno[Deno.internal] is used on purpose here
+const DenoServe = Deno[Deno.internal]?.nodeUnstable?.serve || Deno.serve;
+// @ts-ignore Deno[Deno.internal] is used on purpose here
+const DenoUpgradeHttpRaw = Deno[Deno.internal]?.nodeUnstable?.upgradeHttpRaw ||
+ Deno.upgradeHttpRaw;
+
+const ENCODER = new TextEncoder();
+
+export interface RequestOptions {
+ agent?: Agent;
+ auth?: string;
+ createConnection?: () => unknown;
+ defaultPort?: number;
+ family?: number;
+ headers?: Record<string, string>;
+ hints?: number;
+ host?: string;
+ hostname?: string;
+ insecureHTTPParser?: boolean;
+ localAddress?: string;
+ localPort?: number;
+ lookup?: () => void;
+ maxHeaderSize?: number;
+ method?: string;
+ path?: string;
+ port?: number;
+ protocol?: string;
+ setHost?: boolean;
+ socketPath?: string;
+ timeout?: number;
+ signal?: AbortSignal;
+ href?: string;
+}
+
+// TODO(@bartlomieju): Implement ClientRequest methods (e.g. setHeader())
+/** ClientRequest represents the http(s) request from the client */
+class ClientRequest extends NodeWritable {
+ defaultProtocol = "http:";
+ body: null | ReadableStream = null;
+ controller: ReadableStreamDefaultController | null = null;
+ constructor(
+ public opts: RequestOptions,
+ public cb?: (res: IncomingMessageForClient) => void,
+ ) {
+ super();
+ }
+
+ // deno-lint-ignore no-explicit-any
+ override _write(chunk: any, _enc: string, cb: () => void) {
+ if (this.controller) {
+ this.controller.enqueue(chunk);
+ cb();
+ return;
+ }
+
+ this.body = new ReadableStream({
+ start: (controller) => {
+ this.controller = controller;
+ controller.enqueue(chunk);
+ cb();
+ },
+ });
+ }
+
+ override async _final() {
+ if (this.controller) {
+ this.controller.close();
+ }
+
+ const body = await this._createBody(this.body, this.opts);
+ const client = await this._createCustomClient();
+ const opts = {
+ body,
+ method: this.opts.method,
+ client,
+ headers: this.opts.headers,
+ };
+ const mayResponse = fetch(this._createUrlStrFromOptions(this.opts), opts)
+ .catch((e) => {
+ if (e.message.includes("connection closed before message completed")) {
+ // Node.js seems ignoring this error
+ } else {
+ this.emit("error", e);
+ }
+ return undefined;
+ });
+ const res = new IncomingMessageForClient(
+ await mayResponse,
+ this._createSocket(),
+ );
+ this.emit("response", res);
+ if (client) {
+ res.on("end", () => {
+ client.close();
+ });
+ }
+ this.cb?.(res);
+ }
+
+ abort() {
+ this.destroy();
+ }
+
+ async _createBody(
+ body: ReadableStream | null,
+ opts: RequestOptions,
+ ): Promise<Buffer | ReadableStream | null> {
+ if (!body) return null;
+ if (!opts.headers) return body;
+
+ const headers = Object.fromEntries(
+ Object.entries(opts.headers).map(([k, v]) => [k.toLowerCase(), v]),
+ );
+
+ if (
+ !RE_TE_CHUNKED.test(headers["transfer-encoding"]) &&
+ !Number.isNaN(Number.parseInt(headers["content-length"], 10))
+ ) {
+ const bufferList: Buffer[] = [];
+ for await (const chunk of body) {
+ bufferList.push(chunk);
+ }
+ return Buffer.concat(bufferList);
+ }
+
+ return body;
+ }
+
+ _createCustomClient(): Promise<Deno.HttpClient | undefined> {
+ return Promise.resolve(undefined);
+ }
+
+ _createSocket(): Socket {
+ // Note: Creates a dummy socket for the compatibility
+ // Sometimes the libraries check some properties of socket
+ // e.g. if (!response.socket.authorized) { ... }
+ return new Socket({});
+ }
+
+ _createUrlStrFromOptions(opts: RequestOptions): string {
+ if (opts.href) {
+ return opts.href;
+ }
+ const protocol = opts.protocol ?? this.defaultProtocol;
+ const auth = opts.auth;
+ const host = opts.host ?? opts.hostname ?? "localhost";
+ const defaultPort = opts.agent?.defaultPort;
+ const port = opts.port ?? defaultPort ?? 80;
+ let path = opts.path ?? "/";
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ return `${protocol}//${auth ? `${auth}@` : ""}${host}${
+ port === 80 ? "" : `:${port}`
+ }${path}`;
+ }
+
+ setTimeout() {
+ console.log("not implemented: ClientRequest.setTimeout");
+ }
+}
+
+/** IncomingMessage for http(s) client */
+export class IncomingMessageForClient extends NodeReadable {
+ reader: ReadableStreamDefaultReader | undefined;
+ #statusMessage = "";
+ constructor(public response: Response | undefined, public socket: Socket) {
+ super();
+ this.reader = response?.body?.getReader();
+ }
+
+ override async _read(_size: number) {
+ if (this.reader === undefined) {
+ this.push(null);
+ return;
+ }
+ try {
+ const res = await this.reader.read();
+ if (res.done) {
+ this.push(null);
+ return;
+ }
+ this.push(res.value);
+ } catch (e) {
+ // deno-lint-ignore no-explicit-any
+ this.destroy(e as any);
+ }
+ }
+
+ get headers() {
+ if (this.response) {
+ return Object.fromEntries(this.response.headers.entries());
+ }
+ return {};
+ }
+
+ get trailers() {
+ return {};
+ }
+
+ get statusCode() {
+ return this.response?.status || 0;
+ }
+
+ get statusMessage() {
+ return this.#statusMessage || this.response?.statusText || "";
+ }
+
+ set statusMessage(v: string) {
+ this.#statusMessage = v;
+ }
+}
+
+export class ServerResponse extends NodeWritable {
+ statusCode?: number = undefined;
+ statusMessage?: string = undefined;
+ #headers = new Headers({});
+ #readable: ReadableStream;
+ override writable = true;
+ // used by `npm:on-finished`
+ finished = false;
+ headersSent = false;
+ #firstChunk: Chunk | null = null;
+ // Used if --unstable flag IS NOT present
+ #reqEvent?: Deno.RequestEvent;
+ // Used if --unstable flag IS present
+ #resolve?: (value: Response | PromiseLike<Response>) => void;
+ #isFlashRequest: boolean;
+
+ static #enqueue(controller: ReadableStreamDefaultController, chunk: Chunk) {
+ // TODO(kt3k): This is a workaround for denoland/deno#17194
+ // This if-block should be removed when the above issue is resolved.
+ if (chunk.length === 0) {
+ return;
+ }
+ if (typeof chunk === "string") {
+ controller.enqueue(ENCODER.encode(chunk));
+ } else {
+ controller.enqueue(chunk);
+ }
+ }
+
+ /** Returns true if the response body should be null with the given
+ * http status code */
+ static #bodyShouldBeNull(status: number) {
+ return status === 101 || status === 204 || status === 205 || status === 304;
+ }
+
+ constructor(
+ reqEvent: undefined | Deno.RequestEvent,
+ resolve: undefined | ((value: Response | PromiseLike<Response>) => void),
+ ) {
+ let controller: ReadableByteStreamController;
+ const readable = new ReadableStream({
+ start(c) {
+ controller = c as ReadableByteStreamController;
+ },
+ });
+ super({
+ autoDestroy: true,
+ defaultEncoding: "utf-8",
+ emitClose: true,
+ write: (chunk, _encoding, cb) => {
+ if (!this.headersSent) {
+ if (this.#firstChunk === null) {
+ this.#firstChunk = chunk;
+ return cb();
+ } else {
+ ServerResponse.#enqueue(controller, this.#firstChunk);
+ this.#firstChunk = null;
+ this.respond(false);
+ }
+ }
+ ServerResponse.#enqueue(controller, chunk);
+ return cb();
+ },
+ final: (cb) => {
+ if (this.#firstChunk) {
+ this.respond(true, this.#firstChunk);
+ } else if (!this.headersSent) {
+ this.respond(true);
+ }
+ controller.close();
+ return cb();
+ },
+ destroy: (err, cb) => {
+ if (err) {
+ controller.error(err);
+ }
+ return cb(null);
+ },
+ });
+ this.#readable = readable;
+ this.#resolve = resolve;
+ this.#reqEvent = reqEvent;
+ this.#isFlashRequest = typeof resolve !== "undefined";
+ }
+
+ setHeader(name: string, value: string) {
+ this.#headers.set(name, value);
+ return this;
+ }
+
+ getHeader(name: string) {
+ return this.#headers.get(name);
+ }
+ removeHeader(name: string) {
+ return this.#headers.delete(name);
+ }
+ getHeaderNames() {
+ return Array.from(this.#headers.keys());
+ }
+ hasHeader(name: string) {
+ return this.#headers.has(name);
+ }
+
+ writeHead(status: number, headers: Record<string, string>) {
+ this.statusCode = status;
+ for (const k in headers) {
+ if (Object.hasOwn(headers, k)) {
+ this.#headers.set(k, headers[k]);
+ }
+ }
+ return this;
+ }
+
+ #ensureHeaders(singleChunk?: Chunk) {
+ if (this.statusCode === undefined) {
+ this.statusCode = 200;
+ this.statusMessage = "OK";
+ }
+ // Only taken if --unstable IS NOT present
+ if (
+ !this.#isFlashRequest && typeof singleChunk === "string" &&
+ !this.hasHeader("content-type")
+ ) {
+ this.setHeader("content-type", "text/plain;charset=UTF-8");
+ }
+ }
+
+ respond(final: boolean, singleChunk?: Chunk) {
+ this.headersSent = true;
+ this.#ensureHeaders(singleChunk);
+ let body = singleChunk ?? (final ? null : this.#readable);
+ if (ServerResponse.#bodyShouldBeNull(this.statusCode!)) {
+ body = null;
+ }
+ if (this.#isFlashRequest) {
+ this.#resolve!(
+ new Response(body, {
+ headers: this.#headers,
+ status: this.statusCode,
+ statusText: this.statusMessage,
+ }),
+ );
+ } else {
+ this.#reqEvent!.respondWith(
+ new Response(body, {
+ headers: this.#headers,
+ status: this.statusCode,
+ statusText: this.statusMessage,
+ }),
+ ).catch(() => {
+ // ignore this error
+ });
+ }
+ }
+
+ // deno-lint-ignore no-explicit-any
+ override end(chunk?: any, encoding?: any, cb?: any): this {
+ this.finished = true;
+ if (this.#isFlashRequest) {
+ // Flash sets both of these headers.
+ this.#headers.delete("transfer-encoding");
+ this.#headers.delete("content-length");
+ } else if (!chunk && this.#headers.has("transfer-encoding")) {
+ // FIXME(bnoordhuis) Node sends a zero length chunked body instead, i.e.,
+ // the trailing "0\r\n", but respondWith() just hangs when I try that.
+ this.#headers.set("content-length", "0");
+ this.#headers.delete("transfer-encoding");
+ }
+
+ // @ts-expect-error The signature for cb is stricter than the one implemented here
+ return super.end(chunk, encoding, cb);
+ }
+}
+
+// TODO(@AaronO): optimize
+export class IncomingMessageForServer extends NodeReadable {
+ #req: Request;
+ url: string;
+ method: string;
+
+ constructor(req: Request) {
+ // Check if no body (GET/HEAD/OPTIONS/...)
+ const reader = req.body?.getReader();
+ super({
+ autoDestroy: true,
+ emitClose: true,
+ objectMode: false,
+ read: async function (_size) {
+ if (!reader) {
+ return this.push(null);
+ }
+
+ try {
+ const { value } = await reader!.read();
+ this.push(value !== undefined ? Buffer.from(value) : null);
+ } catch (err) {
+ this.destroy(err as Error);
+ }
+ },
+ destroy: (err, cb) => {
+ reader?.cancel().finally(() => cb(err));
+ },
+ });
+ // TODO(@bartlomieju): consider more robust path extraction, e.g:
+ // url: (new URL(request.url).pathname),
+ this.url = req.url?.slice(req.url.indexOf("/", 8));
+ this.method = req.method;
+ this.#req = req;
+ }
+
+ get aborted() {
+ return false;
+ }
+
+ get httpVersion() {
+ return "1.1";
+ }
+
+ get headers() {
+ return Object.fromEntries(this.#req.headers.entries());
+ }
+
+ get upgrade(): boolean {
+ return Boolean(
+ this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") &&
+ this.#req.headers.get("upgrade"),
+ );
+ }
+}
+
+type ServerHandler = (
+ req: IncomingMessageForServer,
+ res: ServerResponse,
+) => void;
+
+export function Server(handler?: ServerHandler): ServerImpl {
+ return new ServerImpl(handler);
+}
+
+class ServerImpl extends EventEmitter {
+ #isFlashServer: boolean;
+
+ #httpConnections: Set<Deno.HttpConn> = new Set();
+ #listener?: Deno.Listener;
+
+ #addr?: Deno.NetAddr;
+ #hasClosed = false;
+ #ac?: AbortController;
+ #servePromise?: Deferred<void>;
+ listening = false;
+
+ constructor(handler?: ServerHandler) {
+ super();
+ // @ts-ignore Might be undefined without `--unstable` flag
+ this.#isFlashServer = typeof DenoServe == "function";
+ if (this.#isFlashServer) {
+ this.#servePromise = deferred();
+ this.#servePromise.then(() => this.emit("close"));
+ }
+ if (handler !== undefined) {
+ this.on("request", handler);
+ }
+ }
+
+ listen(...args: unknown[]): this {
+ // TODO(bnoordhuis) Delegate to net.Server#listen().
+ const normalized = _normalizeArgs(args);
+ const options = normalized[0] as Partial<ListenOptions>;
+ const cb = normalized[1];
+
+ if (cb !== null) {
+ // @ts-ignore change EventEmitter's sig to use CallableFunction
+ this.once("listening", cb);
+ }
+
+ let port = 0;
+ if (typeof options.port === "number" || typeof options.port === "string") {
+ validatePort(options.port, "options.port");
+ port = options.port | 0;
+ }
+
+ // TODO(bnoordhuis) Node prefers [::] when host is omitted,
+ // we on the other hand default to 0.0.0.0.
+ if (this.#isFlashServer) {
+ const hostname = options.host ?? "0.0.0.0";
+ this.#addr = {
+ hostname,
+ port,
+ } as Deno.NetAddr;
+ this.listening = true;
+ nextTick(() => this.#serve());
+ } else {
+ this.listening = true;
+ const hostname = options.host ?? "";
+ this.#listener = Deno.listen({ port, hostname });
+ nextTick(() => this.#listenLoop());
+ }
+
+ return this;
+ }
+
+ async #listenLoop() {
+ const go = async (httpConn: Deno.HttpConn) => {
+ try {
+ for (;;) {
+ let reqEvent = null;
+ try {
+ // Note: httpConn.nextRequest() calls httpConn.close() on error.
+ reqEvent = await httpConn.nextRequest();
+ } catch {
+ // Connection closed.
+ // TODO(bnoordhuis) Emit "clientError" event on the http.Server
+ // instance? Node emits it when request parsing fails and expects
+ // the listener to send a raw 4xx HTTP response on the underlying
+ // net.Socket but we don't have one to pass to the listener.
+ }
+ if (reqEvent === null) {
+ break;
+ }
+ const req = new IncomingMessageForServer(reqEvent.request);
+ const res = new ServerResponse(reqEvent, undefined);
+ this.emit("request", req, res);
+ }
+ } finally {
+ this.#httpConnections.delete(httpConn);
+ }
+ };
+
+ const listener = this.#listener;
+
+ if (listener !== undefined) {
+ this.emit("listening");
+
+ for await (const conn of listener) {
+ let httpConn: Deno.HttpConn;
+ try {
+ httpConn = Deno.serveHttp(conn);
+ } catch {
+ continue; /// Connection closed.
+ }
+
+ this.#httpConnections.add(httpConn);
+ go(httpConn);
+ }
+ }
+ }
+
+ #serve() {
+ const ac = new AbortController();
+ const handler = (request: Request) => {
+ const req = new IncomingMessageForServer(request);
+ if (req.upgrade && this.listenerCount("upgrade") > 0) {
+ const [conn, head] = DenoUpgradeHttpRaw(request) as [
+ Deno.Conn,
+ Uint8Array,
+ ];
+ const socket = new Socket({
+ handle: new TCP(constants.SERVER, conn),
+ });
+ this.emit("upgrade", req, socket, Buffer.from(head));
+ } else {
+ return new Promise<Response>((resolve): void => {
+ const res = new ServerResponse(undefined, resolve);
+ this.emit("request", req, res);
+ });
+ }
+ };
+
+ if (this.#hasClosed) {
+ return;
+ }
+ this.#ac = ac;
+ DenoServe(
+ {
+ 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");
+ },
+ },
+ ).then(() => this.#servePromise!.resolve());
+ }
+
+ setTimeout() {
+ console.error("Not implemented: Server.setTimeout()");
+ }
+
+ close(cb?: (err?: Error) => void): this {
+ const listening = this.listening;
+ this.listening = false;
+
+ this.#hasClosed = true;
+ if (typeof cb === "function") {
+ if (listening) {
+ this.once("close", cb);
+ } else {
+ this.once("close", function close() {
+ cb(new ERR_SERVER_NOT_RUNNING());
+ });
+ }
+ }
+
+ if (this.#isFlashServer) {
+ if (listening && this.#ac) {
+ this.#ac.abort();
+ this.#ac = undefined;
+ } else {
+ this.#servePromise!.resolve();
+ }
+ } else {
+ nextTick(() => this.emit("close"));
+
+ if (listening) {
+ this.#listener!.close();
+ this.#listener = undefined;
+
+ for (const httpConn of this.#httpConnections) {
+ try {
+ httpConn.close();
+ } catch {
+ // Already closed.
+ }
+ }
+
+ this.#httpConnections.clear();
+ }
+ }
+
+ return this;
+ }
+
+ address() {
+ let addr;
+ if (this.#isFlashServer) {
+ addr = this.#addr!;
+ } else {
+ addr = this.#listener!.addr as Deno.NetAddr;
+ }
+ return {
+ port: addr.port,
+ address: addr.hostname,
+ };
+ }
+}
+
+Server.prototype = ServerImpl.prototype;
+
+export function createServer(handler?: ServerHandler) {
+ return Server(handler);
+}
+
+/** Makes an HTTP request. */
+export function request(
+ url: string | URL,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+export function request(
+ opts: RequestOptions,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+export function request(
+ url: string | URL,
+ opts: RequestOptions,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+// deno-lint-ignore no-explicit-any
+export function request(...args: any[]) {
+ let options = {};
+ if (typeof args[0] === "string") {
+ options = urlToHttpOptions(new URL(args.shift()));
+ } else if (args[0] instanceof URL) {
+ options = urlToHttpOptions(args.shift());
+ }
+ if (args[0] && typeof args[0] !== "function") {
+ Object.assign(options, args.shift());
+ }
+ args.unshift(options);
+ return new ClientRequest(args[0], args[1]);
+}
+
+/** Makes a `GET` HTTP request. */
+export function get(
+ url: string | URL,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+export function get(
+ opts: RequestOptions,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+export function get(
+ url: string | URL,
+ opts: RequestOptions,
+ cb?: (res: IncomingMessageForClient) => void,
+): ClientRequest;
+// deno-lint-ignore no-explicit-any
+export function get(...args: any[]) {
+ const req = request(args[0], args[1], args[2]);
+ req.end();
+ return req;
+}
+
+export {
+ Agent,
+ ClientRequest,
+ IncomingMessageForServer as IncomingMessage,
+ METHODS,
+ OutgoingMessage,
+ STATUS_CODES,
+};
+export default {
+ Agent,
+ ClientRequest,
+ STATUS_CODES,
+ METHODS,
+ createServer,
+ Server,
+ IncomingMessage: IncomingMessageForServer,
+ IncomingMessageForClient,
+ IncomingMessageForServer,
+ OutgoingMessage,
+ ServerResponse,
+ request,
+ get,
+};