fix: address multi-browser routing issues

- Move recording cleanup before connection delete (was no-op)
- Only fallback to default extension when extensionId is null
- Add cwd param to /cli/session/new, CLI passes process.cwd()
- Require pre-existing sessions for execute/reset endpoints
- Remove duplicate ensureRelayServer call in CLI
- Increase fetch timeouts from 500ms to 2000ms
This commit is contained in:
Tommy D. Rossi
2026-02-04 16:51:44 +01:00
parent 44514f00c8
commit 5641b3803b
2 changed files with 34 additions and 44 deletions
+29 -32
View File
@@ -107,9 +107,11 @@ export async function startPlayWriterCDPRelayServer({
} }
const getExtensionConnection = (extensionId?: string | null): ExtensionConnection | null => { const getExtensionConnection = (extensionId?: string | null): ExtensionConnection | null => {
if (extensionId && extensionConnections.has(extensionId)) { // If specific extensionId requested, only return that one (no fallback)
if (extensionId) {
return extensionConnections.get(extensionId) || null return extensionConnections.get(extensionId) || null
} }
// Only fallback to default when no extensionId specified
const fallbackId = getDefaultExtensionId() const fallbackId = getDefaultExtensionId()
if (fallbackId) { if (fallbackId) {
return extensionConnections.get(fallbackId) || null return extensionConnections.get(fallbackId) || null
@@ -1224,6 +1226,15 @@ export async function startPlayWriterCDPRelayServer({
logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'} (${connectionId})`) logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'} (${connectionId})`)
stopExtensionPing(connectionId) stopExtensionPing(connectionId)
// Cancel any active recordings BEFORE removing connection (cancelRecording checks isExtensionConnected)
const recordingRelay = recordingRelays.get(connectionId)
if (recordingRelay) {
recordingRelay.cancelRecording({}).catch(() => {
// Ignore errors during cleanup
})
}
recordingRelays.delete(connectionId)
const connection = extensionConnections.get(connectionId) const connection = extensionConnections.get(connectionId)
if (connection) { if (connection) {
for (const pending of connection.pendingRequests.values()) { for (const pending of connection.pendingRequests.values()) {
@@ -1234,7 +1245,6 @@ export async function startPlayWriterCDPRelayServer({
} }
extensionConnections.delete(connectionId) extensionConnections.delete(connectionId)
recordingRelays.delete(connectionId)
for (const [clientId, client] of playwrightClients.entries()) { for (const [clientId, client] of playwrightClients.entries()) {
if (client.extensionId !== connectionId) { if (client.extensionId !== connectionId) {
@@ -1275,28 +1285,19 @@ export async function startPlayWriterCDPRelayServer({
app.post('/cli/execute', async (c) => { app.post('/cli/execute', async (c) => {
try { try {
const body = await c.req.json() as { sessionId: string; code: string; timeout?: number; cwd?: string } const body = await c.req.json() as { sessionId: string; code: string; timeout?: number }
const { sessionId, code, timeout = 10000, cwd } = body const { sessionId, code, timeout = 10000 } = body
if (!sessionId || !code) { if (!sessionId || !code) {
return c.json({ error: 'sessionId and code are required' }, 400) return c.json({ error: 'sessionId and code are required' }, 400)
} }
const manager = await getExecutorManager() const manager = await getExecutorManager()
const defaultExtension = getExtensionConnection(null) const existingExecutor = manager.getSession(sessionId)
const executor = manager.getExecutor({ if (!existingExecutor) {
sessionId, return c.json({ text: `Session ${sessionId} not found. Run 'playwriter session new' first.`, images: [], isError: true }, 404)
cwd, }
sessionMetadata: { const result = await existingExecutor.execute(code, timeout)
extensionId: defaultExtension?.id || null,
browser: defaultExtension?.info.browser || null,
profile: defaultExtension?.info ? { email: defaultExtension.info.email || '', id: defaultExtension.info.id || '' } : null,
},
})
const result = await executor.execute(code, timeout)
// Increment session counter after each execution to avoid conflicts
nextSessionNumber++
return c.json(result) return c.json(result)
} catch (error: any) { } catch (error: any) {
@@ -1307,25 +1308,19 @@ export async function startPlayWriterCDPRelayServer({
app.post('/cli/reset', async (c) => { app.post('/cli/reset', async (c) => {
try { try {
const body = await c.req.json() as { sessionId: string; cwd?: string } const body = await c.req.json() as { sessionId: string }
const { sessionId, cwd } = body const { sessionId } = body
if (!sessionId) { if (!sessionId) {
return c.json({ error: 'sessionId is required' }, 400) return c.json({ error: 'sessionId is required' }, 400)
} }
const manager = await getExecutorManager() const manager = await getExecutorManager()
const defaultExtension = getExtensionConnection(null) const existingExecutor = manager.getSession(sessionId)
const executor = manager.getExecutor({ if (!existingExecutor) {
sessionId, return c.json({ error: `Session ${sessionId} not found. Run 'playwriter session new' first.` }, 404)
cwd, }
sessionMetadata: { const { page, context } = await existingExecutor.reset()
extensionId: defaultExtension?.id || null,
browser: defaultExtension?.info.browser || null,
profile: defaultExtension?.info ? { email: defaultExtension.info.email || '', id: defaultExtension.info.id || '' } : null,
},
})
const { page, context } = await executor.reset()
return c.json({ return c.json({
success: true, success: true,
@@ -1348,9 +1343,10 @@ export async function startPlayWriterCDPRelayServer({
}) })
app.post('/cli/session/new', async (c) => { app.post('/cli/session/new', async (c) => {
const body = await c.req.json().catch(() => ({})) as { extensionId?: string | null } const body = await c.req.json().catch(() => ({})) as { extensionId?: string | null; cwd?: string }
const sessionId = String(nextSessionNumber++) const sessionId = String(nextSessionNumber++)
const extensionId = body.extensionId || null const extensionId = body.extensionId || null
const cwd = body.cwd
const extension = getExtensionConnection(extensionId) const extension = getExtensionConnection(extensionId)
if (!extension) { if (!extension) {
return c.json({ error: 'Extension not connected' }, 404) return c.json({ error: 'Extension not connected' }, 404)
@@ -1358,6 +1354,7 @@ export async function startPlayWriterCDPRelayServer({
const manager = await getExecutorManager() const manager = await getExecutorManager()
const executor = manager.getExecutor({ const executor = manager.getExecutor({
sessionId, sessionId,
cwd,
sessionMetadata: { sessionMetadata: {
extensionId: extension.id, extensionId: extension.id,
browser: extension.info.browser || null, browser: extension.info.browser || null,
+5 -12
View File
@@ -55,11 +55,11 @@ async function fetchExtensionsStatus(host?: string): Promise<Array<{
try { try {
const serverUrl = await getServerUrl(host) const serverUrl = await getServerUrl(host)
const response = await fetch(`${serverUrl}/extensions/status`, { const response = await fetch(`${serverUrl}/extensions/status`, {
signal: AbortSignal.timeout(500), signal: AbortSignal.timeout(2000),
}) })
if (!response.ok) { if (!response.ok) {
const fallback = await fetch(`${serverUrl}/extension/status`, { const fallback = await fetch(`${serverUrl}/extension/status`, {
signal: AbortSignal.timeout(500), signal: AbortSignal.timeout(2000),
}) })
if (!fallback.ok) { if (!fallback.ok) {
return [] return []
@@ -231,21 +231,14 @@ cli
process.exit(1) process.exit(1)
} }
if (!options.host && !process.env.PLAYWRITER_HOST) {
await ensureRelayServer({ logger: console, env: cliRelayEnv })
const connected = await waitForExtension({ logger: console, timeoutMs: 10000 })
if (!connected) {
console.error('Warning: Extension not connected. Commands may fail.')
}
}
try { try {
const serverUrl = await getServerUrl(options.host) const serverUrl = await getServerUrl(options.host)
const extensionId = selectedExtension.extensionId === 'default' ? null : selectedExtension.extensionId const extensionId = selectedExtension.extensionId === 'default' ? null : selectedExtension.extensionId
const cwd = process.cwd()
const response = await fetch(`${serverUrl}/cli/session/new`, { const response = await fetch(`${serverUrl}/cli/session/new`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ extensionId }), body: JSON.stringify({ extensionId, cwd }),
}) })
if (!response.ok) { if (!response.ok) {
const text = await response.text() const text = await response.text()
@@ -279,7 +272,7 @@ cli
try { try {
const response = await fetch(`${serverUrl}/cli/sessions`, { const response = await fetch(`${serverUrl}/cli/sessions`, {
signal: AbortSignal.timeout(500), signal: AbortSignal.timeout(2000),
}) })
if (!response.ok) { if (!response.ok) {
console.error(`Error: ${response.status} ${await response.text()}`) console.error(`Error: ${response.status} ${await response.text()}`)