fix: improve connection reliability with global timeout and IPv4
- Use 127.0.0.1 instead of localhost to avoid DNS/IPv6 resolution issues - Add 15s global timeout wrapper around connect() to prevent hanging forever - Reduce HTTP retry attempts from 30 to 5 (maintainLoop retries anyway) - Fix WebSocket cleanup: close socket on timeout to prevent orphaned connections - Improve settled state tracking in WebSocket connection promise
This commit is contained in:
+37
-23
@@ -6,7 +6,7 @@ import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
|||||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
||||||
|
|
||||||
const RELAY_PORT = process.env.PLAYWRITER_PORT
|
const RELAY_PORT = process.env.PLAYWRITER_PORT
|
||||||
const RELAY_URL = `ws://localhost:${RELAY_PORT}/extension`
|
const RELAY_URL = `ws://127.0.0.1:${RELAY_PORT}/extension`
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
@@ -34,7 +34,19 @@ class ConnectionManager {
|
|||||||
return this.connectionPromise
|
return this.connectionPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectionPromise = this.connect()
|
// Wrap connect() with a global timeout to ensure it never hangs forever.
|
||||||
|
// This protects against edge cases where individual timeouts don't fire
|
||||||
|
// (e.g., DNS resolution hangs, AbortSignal doesn't work, etc.)
|
||||||
|
const GLOBAL_TIMEOUT_MS = 15000
|
||||||
|
this.connectionPromise = Promise.race([
|
||||||
|
this.connect(),
|
||||||
|
new Promise<never>((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error('Connection timeout (global)'))
|
||||||
|
}, GLOBAL_TIMEOUT_MS)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.connectionPromise
|
await this.connectionPromise
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,22 +55,21 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async connect(): Promise<void> {
|
private async connect(): Promise<void> {
|
||||||
logger.debug(`Waiting for server at http://localhost:${RELAY_PORT}...`)
|
logger.debug(`Waiting for server at http://127.0.0.1:${RELAY_PORT}...`)
|
||||||
|
|
||||||
// Retry for up to 30 seconds with 1s intervals, then give up (maintain loop will retry later)
|
// Retry for up to 5 seconds with 1s intervals, then give up (maintain loop will retry later)
|
||||||
const maxAttempts = 30
|
// Using fewer attempts since maintainLoop retries every 3 seconds anyway
|
||||||
|
const maxAttempts = 5
|
||||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:${RELAY_PORT}`, { method: 'HEAD', signal: AbortSignal.timeout(2000) })
|
await fetch(`http://127.0.0.1:${RELAY_PORT}`, { method: 'HEAD', signal: AbortSignal.timeout(2000) })
|
||||||
logger.debug('Server is available')
|
logger.debug('Server is available')
|
||||||
break
|
break
|
||||||
} catch {
|
} catch {
|
||||||
if (attempt === maxAttempts - 1) {
|
if (attempt === maxAttempts - 1) {
|
||||||
throw new Error('Server not available')
|
throw new Error('Server not available')
|
||||||
}
|
}
|
||||||
if (attempt % 5 === 0) {
|
logger.debug(`Server not available, retrying... (attempt ${attempt + 1}/${maxAttempts})`)
|
||||||
logger.debug(`Server not available, retrying... (attempt ${attempt + 1}/${maxAttempts})`)
|
|
||||||
}
|
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,17 +78,20 @@ class ConnectionManager {
|
|||||||
const socket = new WebSocket(RELAY_URL)
|
const socket = new WebSocket(RELAY_URL)
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let timeoutFired = false
|
let settled = false
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
timeoutFired = true
|
if (settled) return
|
||||||
|
settled = true
|
||||||
logger.debug('WebSocket connection TIMEOUT after 5 seconds')
|
logger.debug('WebSocket connection TIMEOUT after 5 seconds')
|
||||||
|
try {
|
||||||
|
socket.close()
|
||||||
|
} catch {}
|
||||||
reject(new Error('Connection timeout'))
|
reject(new Error('Connection timeout'))
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
if (timeoutFired) {
|
if (settled) return
|
||||||
return
|
settled = true
|
||||||
}
|
|
||||||
logger.debug('WebSocket connected')
|
logger.debug('WebSocket connected')
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
resolve()
|
resolve()
|
||||||
@@ -85,18 +99,18 @@ class ConnectionManager {
|
|||||||
|
|
||||||
socket.onerror = (error) => {
|
socket.onerror = (error) => {
|
||||||
logger.debug('WebSocket error during connection:', error)
|
logger.debug('WebSocket error during connection:', error)
|
||||||
if (!timeoutFired) {
|
if (settled) return
|
||||||
clearTimeout(timeout)
|
settled = true
|
||||||
reject(new Error('WebSocket connection failed'))
|
clearTimeout(timeout)
|
||||||
}
|
reject(new Error('WebSocket connection failed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onclose = (event) => {
|
socket.onclose = (event) => {
|
||||||
logger.debug('WebSocket closed during connection:', { code: event.code, reason: event.reason })
|
logger.debug('WebSocket closed during connection:', { code: event.code, reason: event.reason })
|
||||||
if (!timeoutFired) {
|
if (settled) return
|
||||||
clearTimeout(timeout)
|
settled = true
|
||||||
reject(new Error(`WebSocket closed: ${event.reason || event.code}`))
|
clearTimeout(timeout)
|
||||||
}
|
reject(new Error(`WebSocket closed: ${event.reason || event.code}`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -236,7 +250,7 @@ class ConnectionManager {
|
|||||||
// When replaced by another extension, poll until slot is free
|
// When replaced by another extension, poll until slot is free
|
||||||
if (store.getState().connectionState === 'extension-replaced') {
|
if (store.getState().connectionState === 'extension-replaced') {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:${RELAY_PORT}/extension/status`, { method: 'GET', signal: AbortSignal.timeout(2000) })
|
const response = await fetch(`http://127.0.0.1:${RELAY_PORT}/extension/status`, { method: 'GET', signal: AbortSignal.timeout(2000) })
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!data.connected) {
|
if (!data.connected) {
|
||||||
store.setState({ connectionState: 'idle', errorText: undefined })
|
store.setState({ connectionState: 'idle', errorText: undefined })
|
||||||
|
|||||||
Reference in New Issue
Block a user