Skip to content

Commit 03de2a0

Browse files
authored
fix: handle ECONNRESET errors in Node.js 24.x (#7772)
* fix: handle ECONNRESET errors in Node.js 24.x - Replace wait-port dependency with custom implementation - Add retry logic with ECONNRESET error handling - Add HTTP agent with ECONNRESET handling in proxy - Enable framework host detection for Node 24+ - Update tests to use new wait-port API Fixes Node.js 24.x dev server startup failures * fix: revert detectFrameworkHost change to avoid breaking env injection * fix: correct ECONNRESET handling in waitPort - reject to trigger retry * refactor: remove redundant cleanup function per reviewer feedback * fix: removed proxy.ts handling, setting up for full wait-port.ts handling for econnreset errors * fix: readded prior retry count to see if it works
1 parent a49d5cc commit 03de2a0

File tree

6 files changed

+74
-101
lines changed

6 files changed

+74
-101
lines changed

package-lock.json

Lines changed: 0 additions & 76 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@
150150
"ulid": "3.0.1",
151151
"update-notifier": "7.3.1",
152152
"uuid": "11.1.0",
153-
"wait-port": "1.1.0",
154153
"write-file-atomic": "5.0.1",
155154
"ws": "8.18.3"
156155
},

src/lib/http-agent.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { readFile } from 'fs/promises'
22

33
import { HttpsProxyAgent } from 'https-proxy-agent'
4-
import waitPort from 'wait-port'
54

65
import { NETLIFYDEVERR, NETLIFYDEVWARN, exit, log } from '../utils/command-helpers.js'
6+
import { waitPort } from './wait-port.js'
77

88
// https://github.com/TooTallNate/node-https-proxy-agent/issues/89
99
// Maybe replace with https://github.com/delvedor/hpagent
@@ -29,7 +29,7 @@ class HttpsProxyAgentWithCA extends HttpsProxyAgent {
2929
const DEFAULT_HTTP_PORT = 80
3030
const DEFAULT_HTTPS_PORT = 443
3131
// 50 seconds
32-
const AGENT_PORT_TIMEOUT = 50
32+
const AGENT_PORT_TIMEOUT = 50_000
3333

3434
export const tryGetAgent = async ({
3535
certificateFile,
@@ -66,12 +66,11 @@ export const tryGetAgent = async ({
6666

6767
let port
6868
try {
69-
port = await waitPort({
70-
port: Number.parseInt(proxyUrl.port) || (scheme === 'http' ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT),
71-
host: proxyUrl.hostname,
72-
timeout: AGENT_PORT_TIMEOUT,
73-
output: 'silent',
74-
})
69+
port = await waitPort(
70+
Number.parseInt(proxyUrl.port) || (scheme === 'http' ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT),
71+
proxyUrl.hostname,
72+
AGENT_PORT_TIMEOUT,
73+
)
7574
} catch (error) {
7675
// unknown error
7776
// @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.

src/lib/wait-port.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import net from 'net'
2+
3+
export const waitPort = async (
4+
port: number,
5+
host: string,
6+
timeout: number,
7+
maxRetries?: number,
8+
): Promise<{ open: boolean; ipVersion?: 4 | 6 }> => {
9+
const startTime = Date.now()
10+
const retries = maxRetries ?? Math.ceil(timeout / 2000)
11+
12+
for (let attempt = 0; attempt < retries; attempt++) {
13+
if (Date.now() - startTime > timeout) {
14+
return { open: false }
15+
}
16+
17+
try {
18+
const ipVersion = await new Promise<4 | 6>((resolve, reject) => {
19+
const socket = new net.Socket()
20+
let isResolved = false
21+
22+
socket.on('connect', () => {
23+
isResolved = true
24+
// Detect actual IP version from the connection
25+
const detectedVersion = socket.remoteFamily === 'IPv6' ? 6 : 4
26+
socket.end()
27+
resolve(detectedVersion)
28+
})
29+
30+
socket.on('error', (error) => {
31+
if (!isResolved) {
32+
isResolved = true
33+
socket.destroy()
34+
reject(error)
35+
}
36+
})
37+
38+
socket.setTimeout(1000, () => {
39+
if (!isResolved) {
40+
isResolved = true
41+
socket.destroy()
42+
reject(new Error('Socket timeout'))
43+
}
44+
})
45+
46+
socket.connect(port, host)
47+
})
48+
49+
return { open: true, ipVersion }
50+
} catch {
51+
await new Promise((resolve) => setTimeout(resolve, Math.min(100 * (attempt + 1), 1000)))
52+
continue
53+
}
54+
}
55+
56+
return { open: false }
57+
}

src/utils/framework-server.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { rm } from 'node:fs/promises'
22

3-
import waitPort from 'wait-port'
4-
53
import { startSpinner, stopSpinner } from '../lib/spinner.js'
4+
import { waitPort } from '../lib/wait-port.js'
65

76
import { logAndThrowError, log, NETLIFYDEVERR, NETLIFYDEVLOG, chalk } from './command-helpers.js'
87
import { runCommand } from './shell.js'
@@ -56,14 +55,13 @@ export const startFrameworkServer = async function ({
5655
const ipVersion = parseInt(process.versions.node.split('.')[0]) >= 18 ? 6 : 4
5756
port = { open: true, ipVersion }
5857
} else {
59-
const waitPortPromise = waitPort({
58+
const waitPortPromise = waitPort(
6059
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61-
port: settings.frameworkPort!,
62-
host: 'localhost',
63-
output: 'silent',
64-
timeout: FRAMEWORK_PORT_TIMEOUT_MS,
65-
...(settings.pollingStrategies?.includes('HTTP') && { protocol: 'http' }),
66-
})
60+
settings.frameworkPort!,
61+
'localhost',
62+
FRAMEWORK_PORT_TIMEOUT_MS,
63+
20,
64+
)
6765

6866
const timerId = setTimeout(() => {
6967
if (!port?.open) {

tests/integration/commands/functions-serve/functions-serve.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import getPort from 'get-port'
55
import fetch from 'node-fetch'
66
import semver from 'semver'
77
import { describe, test } from 'vitest'
8-
import waitPort from 'wait-port'
98

9+
import { waitPort } from '../../../../src/lib/wait-port.js'
1010
import { cliPath } from '../../utils/cli-path.js'
1111
import { withMockApi } from '../../utils/mock-api.js'
1212
import { type SiteBuilder, withSiteBuilder } from '../../utils/site-builder.js'
@@ -48,12 +48,8 @@ const withFunctionsServer = async (
4848
console.log(data.toString())
4949
})
5050

51-
const { open } = await waitPort({
52-
port,
53-
output: 'silent',
54-
timeout: SERVE_TIMEOUT,
55-
})
56-
if (!open) {
51+
const result = await waitPort(port, 'localhost', SERVE_TIMEOUT)
52+
if (!result.open) {
5753
throw new Error('Timed out waiting for functions server')
5854
}
5955
return await testHandler()

0 commit comments

Comments
 (0)