#1909·pest

[pest-plugin-browser] Client::execute() loops forever once the Playwright websocket is closed (run hangs at 100% CPU or exhausts memory)

Author: yarbalaCreated Sep 14, 2026Updated Sep 14, 2026

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:

php
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 — a pest --coverage run sat like that for 40 minutes with no output;
  • consumers that call iterator_to_array() (processVoidResponse: goto, close, …) accumulate null entries until memory_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

  1. Any browser test file with a handful of tests.
  2. While it runs, kill the Playwright server: pkill -9 -f "playwright run-server".
  3. 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():

php
$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.