[pest-plugin-browser] Client::execute() loops forever once the Playwright websocket is closed (run hangs at 100% CPU or exhausts memory)
This concerns pestphp/pest-plugin-browser (4.3.1; the same code is on its 5.x branch, 5.0.1). That repository has issues disabled, hence the report here.
What happens
When the browser or the playwright run-server process dies during a run, every later call into Playwright hangs the PHP process instead of failing.
Pest\Browser\Playwright\Client::execute() reads responses in a while (true) loop:
while (true) {
$responseJson = $this->fetch($this->websocketConnection); // (string) $client->receive()?->read()
$response = json_decode($responseJson, true);
...
yield $response;
if ((isset($response['id']) && $response['id'] === $requestId) || ...) {
break;
}
}Amp\Websocket\Client\WebsocketConnection::receive() returns null once the connection is closed, so fetch() returns '', json_decode('') returns null, nothing matches the request id, and the loop never ends:
- consumers that iterate the generator with
foreach(processResultResponse,querySelectorAll, locator counts, …) spin at 100% CPU forever — apest --coveragerun sat like that for 40 minutes with no output; - consumers that call
iterator_to_array()(processVoidResponse:goto,close, …) accumulatenullentries untilmemory_limit:Allowed memory size of 4294967296 bytes exhausted (tried to allocate 4294967304 bytes) at src/Playwright/Concerns/InteractsWithPlaywright.php:123.
The first symptom in our log was Amp\Websocket\WebsocketClosedException: Client unexpectedly closed; Code 1006 (ABNORMAL_CLOSE); Reason: "Writing to the client failed" thrown by sendText(); the next execute() call reached the read loop and never returned.
Reproduction
- Any browser test file with a handful of tests.
- While it runs, kill the Playwright server:
pkill -9 -f "playwright run-server". - The test running at that moment never finishes (CPU at 100%), or the process dies with the memory error above.
Seen with pestphp/pest-plugin-browser 4.3.1 (Pest 4.7.5, PHP 8.4, Chromium via playwright run-server --mode launchServer); the loop is unchanged on 5.x (5.0.1).
Suggested fix
Treat a closed connection as an error instead of an empty response, e.g. in execute():
$message = $this->websocketConnection->receive();
if ($message === null) {
throw new BrowserAlreadyClosedException(); // or a dedicated exception naming the closed connection
}
$responseJson = $message->read();With that, the test that lost the browser fails at once with a clear reason, and the following tests fail the same way instead of hanging the run.
Source: pestphp/pest