diff --git a/net/deno.json b/net/deno.json index fb8b22f69794..21f0365e259d 100644 --- a/net/deno.json +++ b/net/deno.json @@ -4,6 +4,7 @@ "exports": { ".": "./mod.ts", "./get-available-port": "./get_available_port.ts", + "./unstable-is-port-available": "./unstable_is_port_available.ts", "./unstable-ip": "./unstable_ip.ts", "./get-network-address": "./unstable_get_network_address.ts", "./unstable-get-network-address": "./unstable_get_network_address.ts" diff --git a/net/unstable_is_port_available.ts b/net/unstable_is_port_available.ts new file mode 100644 index 000000000000..18f5da44c8e1 --- /dev/null +++ b/net/unstable_is_port_available.ts @@ -0,0 +1,78 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +/** + * Options for {@linkcode isPortAvailable}. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + */ +export interface IsPortAvailableOptions { + /** + * The hostname to check the port on. + * + * @default {"0.0.0.0"} + */ + hostname?: string; +} + +/** + * Returns whether the given TCP port is available to listen on. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * > [!IMPORTANT] + * > This check is inherently racy: the port may be taken by another process + * > between the time this function returns and the time you attempt to listen + * > on it. When you control the listener, prefer passing `port: 0` to + * > {@linkcode Deno.serve} or {@linkcode Deno.listen} to let the operating + * > system assign an available port, or use + * > {@linkcode https://jsr.io/@std/net/doc/get-available-port/~/getAvailablePort | getAvailablePort}. + * + * This function requires the `--allow-net` permission. Any error other than + * {@linkcode Deno.errors.AddrInUse} is rethrown, including + * {@linkcode Deno.errors.PermissionDenied} for privileged ports (below 1024) + * or when the `net` permission has not been granted. + * + * @param port The port to check. Use `0` to check whether the operating system + * can assign an ephemeral port. + * @param options Options for checking the port. + * @returns `true` if the port is available to listen on, `false` if it is + * already in use. + * + * @example Usage + * ```ts + * import { isPortAvailable } from "@std/net/unstable-is-port-available"; + * import { assert } from "@std/assert"; + * + * using listener = Deno.listen({ port: 0 }); + * const { port } = listener.addr; + * + * assert(!isPortAvailable(port)); + * ``` + * + * @example Check before serving + * ```ts no-assert ignore + * import { isPortAvailable } from "@std/net/unstable-is-port-available"; + * + * if (isPortAvailable(8080)) { + * Deno.serve({ port: 8080 }, () => new Response("Hello, world!")); + * } + * ``` + */ +export function isPortAvailable( + port: number, + options?: IsPortAvailableOptions, +): boolean { + try { + using _listener = Deno.listen( + options?.hostname !== undefined + ? { port, hostname: options.hostname } + : { port }, + ); + return true; + } catch (e) { + if (e instanceof Deno.errors.AddrInUse) { + return false; + } + throw e; + } +} diff --git a/net/unstable_is_port_available_test.ts b/net/unstable_is_port_available_test.ts new file mode 100644 index 000000000000..e98490675242 --- /dev/null +++ b/net/unstable_is_port_available_test.ts @@ -0,0 +1,28 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { isPortAvailable } from "./unstable_is_port_available.ts"; +import { assert, assertFalse, assertThrows } from "@std/assert"; +import { stub } from "@std/testing/mock"; + +Deno.test("isPortAvailable() returns true for an available port", () => { + assert(isPortAvailable(0)); +}); + +Deno.test("isPortAvailable() returns false for a port already in use", () => { + using listener = Deno.listen({ port: 0 }); + const { port } = listener.addr; + assertFalse(isPortAvailable(port)); +}); + +Deno.test("isPortAvailable() respects the hostname option", () => { + using listener = Deno.listen({ port: 0, hostname: "127.0.0.1" }); + const { port } = listener.addr; + assertFalse(isPortAvailable(port, { hostname: "127.0.0.1" })); +}); + +Deno.test("isPortAvailable() rethrows errors other than AddrInUse", () => { + using _listen = stub(Deno, "listen", () => { + throw new Error("boom"); + }); + assertThrows(() => isPortAvailable(0), Error, "boom"); +});