diff --git a/package-lock.json b/package-lock.json index d38ebf4d85e..e4c4f9c48cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,6 @@ "ulid": "3.0.1", "update-notifier": "7.3.1", "uuid": "11.1.0", - "wait-port": "1.1.0", "write-file-atomic": "5.0.1", "ws": "8.18.3" }, @@ -18022,81 +18021,6 @@ } } }, - "node_modules/wait-port": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", - "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", - "dependencies": { - "chalk": "^4.1.2", - "commander": "^9.3.0", - "debug": "^4.3.4" - }, - "bin": { - "wait-port": "bin/wait-port.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/wait-port/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wait-port/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/wait-port/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wait-port/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/wait-port/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 0a7f11ea3f4..2c49aa09d06 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "ulid": "3.0.1", "update-notifier": "7.3.1", "uuid": "11.1.0", - "wait-port": "1.1.0", "write-file-atomic": "5.0.1", "ws": "8.18.3" }, diff --git a/src/lib/http-agent.ts b/src/lib/http-agent.ts index fd1b0f7312f..d9f44df9b65 100644 --- a/src/lib/http-agent.ts +++ b/src/lib/http-agent.ts @@ -1,9 +1,9 @@ import { readFile } from 'fs/promises' import { HttpsProxyAgent } from 'https-proxy-agent' -import waitPort from 'wait-port' import { NETLIFYDEVERR, NETLIFYDEVWARN, exit, log } from '../utils/command-helpers.js' +import { waitPort } from './wait-port.js' // https://github.com/TooTallNate/node-https-proxy-agent/issues/89 // Maybe replace with https://github.com/delvedor/hpagent @@ -29,7 +29,7 @@ class HttpsProxyAgentWithCA extends HttpsProxyAgent { const DEFAULT_HTTP_PORT = 80 const DEFAULT_HTTPS_PORT = 443 // 50 seconds -const AGENT_PORT_TIMEOUT = 50 +const AGENT_PORT_TIMEOUT = 50_000 export const tryGetAgent = async ({ certificateFile, @@ -66,12 +66,11 @@ export const tryGetAgent = async ({ let port try { - port = await waitPort({ - port: Number.parseInt(proxyUrl.port) || (scheme === 'http' ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT), - host: proxyUrl.hostname, - timeout: AGENT_PORT_TIMEOUT, - output: 'silent', - }) + port = await waitPort( + Number.parseInt(proxyUrl.port) || (scheme === 'http' ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT), + proxyUrl.hostname, + AGENT_PORT_TIMEOUT, + ) } catch (error) { // unknown error // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. diff --git a/src/lib/wait-port.ts b/src/lib/wait-port.ts new file mode 100644 index 00000000000..c3aa66e6b03 --- /dev/null +++ b/src/lib/wait-port.ts @@ -0,0 +1,57 @@ +import net from 'net' + +export const waitPort = async ( + port: number, + host: string, + timeout: number, + maxRetries?: number, +): Promise<{ open: boolean; ipVersion?: 4 | 6 }> => { + const startTime = Date.now() + const retries = maxRetries ?? Math.ceil(timeout / 2000) + + for (let attempt = 0; attempt < retries; attempt++) { + if (Date.now() - startTime > timeout) { + return { open: false } + } + + try { + const ipVersion = await new Promise<4 | 6>((resolve, reject) => { + const socket = new net.Socket() + let isResolved = false + + socket.on('connect', () => { + isResolved = true + // Detect actual IP version from the connection + const detectedVersion = socket.remoteFamily === 'IPv6' ? 6 : 4 + socket.end() + resolve(detectedVersion) + }) + + socket.on('error', (error) => { + if (!isResolved) { + isResolved = true + socket.destroy() + reject(error) + } + }) + + socket.setTimeout(1000, () => { + if (!isResolved) { + isResolved = true + socket.destroy() + reject(new Error('Socket timeout')) + } + }) + + socket.connect(port, host) + }) + + return { open: true, ipVersion } + } catch { + await new Promise((resolve) => setTimeout(resolve, Math.min(100 * (attempt + 1), 1000))) + continue + } + } + + return { open: false } +} diff --git a/src/utils/framework-server.ts b/src/utils/framework-server.ts index 594096af4f6..bb7f11e83b7 100644 --- a/src/utils/framework-server.ts +++ b/src/utils/framework-server.ts @@ -1,8 +1,7 @@ import { rm } from 'node:fs/promises' -import waitPort from 'wait-port' - import { startSpinner, stopSpinner } from '../lib/spinner.js' +import { waitPort } from '../lib/wait-port.js' import { logAndThrowError, log, NETLIFYDEVERR, NETLIFYDEVLOG, chalk } from './command-helpers.js' import { runCommand } from './shell.js' @@ -56,14 +55,13 @@ export const startFrameworkServer = async function ({ const ipVersion = parseInt(process.versions.node.split('.')[0]) >= 18 ? 6 : 4 port = { open: true, ipVersion } } else { - const waitPortPromise = waitPort({ + const waitPortPromise = waitPort( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - port: settings.frameworkPort!, - host: 'localhost', - output: 'silent', - timeout: FRAMEWORK_PORT_TIMEOUT_MS, - ...(settings.pollingStrategies?.includes('HTTP') && { protocol: 'http' }), - }) + settings.frameworkPort!, + 'localhost', + FRAMEWORK_PORT_TIMEOUT_MS, + 20, + ) const timerId = setTimeout(() => { if (!port?.open) { diff --git a/tests/integration/commands/functions-serve/functions-serve.test.ts b/tests/integration/commands/functions-serve/functions-serve.test.ts index 07cada07d02..09a8c1ae326 100644 --- a/tests/integration/commands/functions-serve/functions-serve.test.ts +++ b/tests/integration/commands/functions-serve/functions-serve.test.ts @@ -5,8 +5,8 @@ import getPort from 'get-port' import fetch from 'node-fetch' import semver from 'semver' import { describe, test } from 'vitest' -import waitPort from 'wait-port' +import { waitPort } from '../../../../src/lib/wait-port.js' import { cliPath } from '../../utils/cli-path.js' import { withMockApi } from '../../utils/mock-api.js' import { type SiteBuilder, withSiteBuilder } from '../../utils/site-builder.js' @@ -48,12 +48,8 @@ const withFunctionsServer = async ( console.log(data.toString()) }) - const { open } = await waitPort({ - port, - output: 'silent', - timeout: SERVE_TIMEOUT, - }) - if (!open) { + const result = await waitPort(port, 'localhost', SERVE_TIMEOUT) + if (!result.open) { throw new Error('Timed out waiting for functions server') } return await testHandler()