From 3c28eb2623116484aa4dd88c529bf06e3e61bd27 Mon Sep 17 00:00:00 2001 From: kitsuyui Date: Thu, 4 Jun 2026 06:15:21 +0900 Subject: [PATCH] fix: restart browser automatically on disconnect in server mode When the Playwright browser crashes or disconnects (OOM, runtime error), listen for the 'disconnected' event and restart it automatically. The server handler reads from a mutable browser reference via a getter closure, so in-flight requests pick up the new browser after restart. Also rename createServer/createHandler to accept a browser getter (() => Browser) instead of a direct Browser instance, enabling the mutable reference to be shared across reconnects. --- src/server/index.spec.ts | 14 +++++++------- src/server/index.ts | 41 ++++++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/server/index.spec.ts b/src/server/index.spec.ts index 62a6563d..ce38d963 100644 --- a/src/server/index.spec.ts +++ b/src/server/index.spec.ts @@ -93,7 +93,7 @@ describe('withServer', () => { const browser = await getBrowser() defer(() => browser.close()) - const server = await createServer({ browser, port }) + const server = await createServer({ getBrowserFn: () => browser, port }) defer(() => server.close()) const res: IncomingMessage = await new Promise((resolve) => { @@ -137,7 +137,7 @@ describe('withServer', () => { const browser = await getBrowser() defer(() => browser.close()) - const server = await createServer({ browser, port }) + const server = await createServer({ getBrowserFn: () => browser, port }) defer(() => server.close()) const res: IncomingMessage = await new Promise((resolve) => { @@ -153,7 +153,7 @@ describe('withServer', () => { it('returns 502 when browser becomes unavailable', async () => { const port = 8093 const browser = await getBrowser() - const server = await createServer({ browser, port }) + const server = await createServer({ getBrowserFn: () => browser, port }) try { await browser.close() @@ -178,7 +178,7 @@ describe('withServer', () => { const browser = await getBrowser() defer(() => browser.close()) - const server = await createServer({ browser, port }) + const server = await createServer({ getBrowserFn: () => browser, port }) defer(() => server.close()) const res: IncomingMessage = await new Promise((resolve) => { @@ -199,7 +199,7 @@ describe('withServer', () => { defer(() => browser.close()) const server = await createServer({ - browser, + getBrowserFn: () => browser, port, maxConcurrentRenders: 0, }) @@ -222,7 +222,7 @@ describe('withServer', () => { defer(() => browser.close()) const server = await createServer({ - browser, + getBrowserFn: () => browser, port, maxConcurrentRenders: 0, }) @@ -245,7 +245,7 @@ describe('withServer', () => { defer(() => browser.close()) const server = await createServer({ - browser, + getBrowserFn: () => browser, port, maxConcurrentRenders: 0, }) diff --git a/src/server/index.ts b/src/server/index.ts index 0bae280d..a4c2fc77 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -126,7 +126,7 @@ function replyWithBadGateway(res: http.ServerResponse): void { } async function renderWithConcurrencyLimit( - browser: Browser, + getBrowserFn: () => Browser, req: http.IncomingMessage, res: http.ServerResponse, counter: { value: number }, @@ -139,18 +139,17 @@ async function renderWithConcurrencyLimit( } counter.value++ try { - await respondToIncomingRequest(browser, req, res) + await respondToIncomingRequest(getBrowserFn(), req, res) } finally { counter.value-- } } export function createHandler( - browser: Browser, + getBrowserFn: () => Browser, { maxConcurrentRenders = 10 }: { maxConcurrentRenders?: number } = {}, ) { const counter = { value: 0 } - return function renderHandler( req: http.IncomingMessage, res: http.ServerResponse, @@ -159,28 +158,28 @@ export function createHandler( const task = parsedRequest.type === 'render' ? renderWithConcurrencyLimit( - browser, + getBrowserFn, req, res, counter, maxConcurrentRenders, ) - : respondToIncomingRequest(browser, req, res) + : respondToIncomingRequest(getBrowserFn(), req, res) task.catch(() => replyWithBadGateway(res)) } } export async function createServer({ - browser, + getBrowserFn, port = 8080, maxConcurrentRenders = 10, }: { - browser: Browser + getBrowserFn: () => Browser port: number maxConcurrentRenders?: number }): Promise { const server = http.createServer( - createHandler(browser, { maxConcurrentRenders }), + createHandler(getBrowserFn, { maxConcurrentRenders }), ) await new Promise((resolve) => { server.listen(port, () => { @@ -225,10 +224,28 @@ export async function main({ maxConcurrentRenders = 10, }: ServerArgument = {}): Promise { await runWithDefer(async (defer) => { - const browser = await getBrowser({ name, headless }) - defer(() => browser.close()) + let activeBrowser = await getBrowser({ name, headless }) + + const onDisconnected = async () => { + try { + activeBrowser = await getBrowser({ name, headless }) + activeBrowser.on('disconnected', onDisconnected) + } catch { + // Restart failed; subsequent requests will error naturally + } + } + activeBrowser.on('disconnected', onDisconnected) + + defer(async () => { + activeBrowser.removeListener('disconnected', onDisconnected) + await activeBrowser.close() + }) - const server = await createServer({ browser, port, maxConcurrentRenders }) + const server = await createServer({ + getBrowserFn: () => activeBrowser, + port, + maxConcurrentRenders, + }) defer(() => closeServerGracefully(server)) await waitForProcessExit()