fix: route CDP clients through live extension bindings

When an extension reconnects, clients can be rebound to a successor connection without reopening the Playwright socket. Command routing was still resolving via the extension id captured at client socket open, so rebound clients could hit stale routes and return Extension not connected.

Resolve the bound extension id from relay state on every incoming Playwright message, and prefer the newest same-stableKey successor when rebinding on extension close. This keeps reconnect handoff consistent between state transitions and runtime routing.
This commit is contained in:
Tommy D. Rossi
2026-02-27 09:01:53 +01:00
parent 44212ceda8
commit 6807209e85
+44 -33
View File
@@ -932,6 +932,11 @@ export async function startPlayWriterCDPRelayServer({
: getExtensionConnection(null, { allowFallback: true })
const clientExtensionId = resolvedExtension?.id || null
const getBoundExtensionIdForClient = (): string | null => {
const client = store.getState().playwrightClients.get(clientId)
return client?.extensionId || null
}
return {
async onOpen(_event, ws) {
if (store.getState().playwrightClients.has(clientId)) {
@@ -990,7 +995,8 @@ export async function startPlayWriterCDPRelayServer({
emitter.emit('cdp:command', { clientId, command: message })
const extensionConn = getExtensionConnection(clientExtensionId)
const boundExtensionId = getBoundExtensionIdForClient()
const extensionConn = getExtensionConnection(boundExtensionId)
if (!extensionConn) {
sendToPlaywright({
message: {
@@ -1089,39 +1095,39 @@ export async function startPlayWriterCDPRelayServer({
const attachResponse = result as Protocol.Target.AttachToTargetResponse | undefined
const attachRequestParams = params as Protocol.Target.AttachToTargetRequest | undefined
if (attachResponse?.sessionId) {
const freshExt3 = store.getState().extensions.get(extensionConn.id)
const freshTargets3 = freshExt3?.connectedTargets || new Map()
const target = Array.from(freshTargets3.values()).find(
(t) => t.targetId === attachRequestParams?.targetId,
)
if (target) {
const attachedPayload = {
method: 'Target.attachedToTarget',
params: {
sessionId: attachResponse.sessionId,
targetInfo: {
...target.targetInfo,
attached: true,
const freshExt3 = store.getState().extensions.get(extensionConn.id)
const freshTargets3 = freshExt3?.connectedTargets || new Map()
const target = Array.from(freshTargets3.values()).find((t) => {
return t.targetId === attachRequestParams?.targetId
})
if (target) {
const attachedPayload = {
method: 'Target.attachedToTarget',
params: {
sessionId: attachResponse.sessionId,
targetInfo: {
...target.targetInfo,
attached: true,
},
waitingForDebugger: false,
},
waitingForDebugger: false,
},
} satisfies CDPEventFor<'Target.attachedToTarget'>
if (!target.targetInfo.url) {
logger?.error(
pc.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'),
} satisfies CDPEventFor<'Target.attachedToTarget'>
if (!target.targetInfo.url) {
logger?.error(
pc.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'),
JSON.stringify(attachedPayload),
)
}
logger?.log(
pc.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'),
JSON.stringify(attachedPayload),
)
sendToPlaywright({
message: attachedPayload,
clientId,
source: 'server',
})
}
logger?.log(
pc.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'),
JSON.stringify(attachedPayload),
)
sendToPlaywright({
message: attachedPayload,
clientId,
source: 'server',
})
}
}
}
@@ -1572,10 +1578,15 @@ export async function startPlayWriterCDPRelayServer({
const currentRelayState = store.getState()
const closingExtension = currentRelayState.extensions.get(connectionId)
const successorCandidates = closingExtension
? Array.from(currentRelayState.extensions.values())
.reverse()
.filter((ext) => {
return ext.id !== connectionId && ext.stableKey === closingExtension.stableKey && Boolean(ext.ws)
})
: []
const successorExtension = closingExtension
? Array.from(currentRelayState.extensions.values()).find((ext) => {
return ext.id !== connectionId && ext.stableKey === closingExtension.stableKey
})
? successorCandidates[0]
: undefined
if (successorExtension) {