Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions net/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
78 changes: 78 additions & 0 deletions net/unstable_is_port_available.ts
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth documenting here that availability is per-interface, not global — the current wording reads as though a port is simply free or not.

With the default 0.0.0.0, a process already listening on 127.0.0.1:8080 makes this return false on Linux and macOS, but the same situation can return true on Windows, which has different address-conflict rules. So the same code gives different answers per platform, and neither answer is wrong.

Related: localhost resolving to ::1 versus 127.0.0.1 will also change the result, which matters as soon as someone passes hostname: "localhost".

Also worth saying plainly that this is TCP-only. There's no transport option and UDP occupies a separate port namespace, so isPortAvailable(53) says nothing about a DNS server on UDP 53 — the name doesn't hint at that restriction, only the prose does.

* {@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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: documenting 0 as a supported input is a bit odd, since "can the OS assign an ephemeral port" is essentially always true and isn't a question anyone needs this function to answer. Might be cleaner to leave it undocumented, or reject it, than to bless an input whose answer is a constant.

* 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)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example demonstrates exactly the pattern the [!IMPORTANT] block thirty lines above tells people not to use — check, then bind separately, with a window in between where another process can take the port.

A warning followed by a worked example of the thing being warned about mostly teaches the example. People copy code blocks; they skim prose.

I'd drop this example entirely, or replace it with the safe shape (Deno.serve({ port: 0 }) and read the assigned port back). If there's a legitimate check-then-bind use case worth showing, it'd need to be one where losing the race is acceptable and the example says so explicitly.

* 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;
}
}
28 changes: 28 additions & 0 deletions net/unstable_is_port_available_test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The true case is tautological as written: port: 0 asks the OS for any ephemeral port, which essentially always succeeds, so this passes even if the implementation were wrong.

A test with real signal: listen on port 0, capture the assigned port, close the listener, then assert isPortAvailable(capturedPort) is true. That exercises the actual "this specific port is free" path, and pairs nicely with the existing in-use test which covers the inverse.

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");
});
Loading