diff --git a/apps/playwright-browser-tunnel/src/HttpServer.ts b/apps/playwright-browser-tunnel/src/HttpServer.ts index 8f57bff9e0..c87db4dd1a 100644 --- a/apps/playwright-browser-tunnel/src/HttpServer.ts +++ b/apps/playwright-browser-tunnel/src/HttpServer.ts @@ -2,12 +2,24 @@ // See LICENSE in the project root for license information. import http from 'node:http'; +import type { AddressInfo } from 'node:net'; -import { WebSocketServer, type WebSocket, type AddressInfo } from 'ws'; +import { WebSocketServer, type WebSocket } from 'ws'; import type { ITerminal } from '@rushstack/terminal'; -const LOCALHOST_IP: string = '127.0.0.1'; +const LOCALHOST: string = 'localhost'; + +/** + * Formats an address info object into a WebSocket-compatible address string. + * IPv6 addresses are formatted with brackets: [address]:port + * IPv4 addresses are formatted as: address:port + */ +function formatAddress(addressInfo: AddressInfo): string { + return addressInfo.family === 'IPv6' + ? `[${addressInfo.address}]:${addressInfo.port}` + : `${addressInfo.address}:${addressInfo.port}`; +} /** * This HttpServer is used for the localProxyWs WebSocketServer. @@ -17,7 +29,7 @@ const LOCALHOST_IP: string = '127.0.0.1'; export class HttpServer { private readonly _server: http.Server; private readonly _wsServer: WebSocketServer; // local proxy websocket server accepting browser clients - private _listeningPort: number | undefined; + private _listeningAddress: string | undefined; private _logger: ITerminal; public constructor(logger: ITerminal) { @@ -37,22 +49,32 @@ export class HttpServer { public listen(): Promise { return new Promise((resolve) => { - this._server.listen(0, LOCALHOST_IP, () => { - this._listeningPort = (this._server.address() as AddressInfo).port; + // Bind to 'localhost' which resolves to IPv4 (127.0.0.1) or IPv6 (::1) + // depending on system configuration and DNS resolution + this._server.listen(0, LOCALHOST, () => { + const addressInfo = this._server.address(); + if (!addressInfo) { + throw new Error('Server address is null - server may not be bound properly'); + } + if (typeof addressInfo === 'string') { + throw new Error( + `Server address is a pipe/socket path (${addressInfo}), expected an IP address` + ); + } + const formattedAddress: string = formatAddress(addressInfo); + this._listeningAddress = formattedAddress; // This MUST be printed to terminal so VS Code can auto-port forward - this._logger.writeLine( - `Local proxy HttpServer listening at ws://${LOCALHOST_IP}:${this._listeningPort}` - ); + this._logger.writeLine(`Local proxy HttpServer listening at ws://${formattedAddress}`); resolve(); }); }); } public get endpoint(): string { - if (this._listeningPort === undefined) { + if (this._listeningAddress === undefined) { throw new Error('HttpServer not listening yet'); } - return `ws://${LOCALHOST_IP}:${this._listeningPort}`; + return `ws://${this._listeningAddress}`; } public get wsServer(): WebSocketServer {