Affected version: dev-main (2026-07)
Severity: critical — long-running processes eventually fail every request with a misleading timeout
Summary
ProcessJsonRpcClient::readProcessOutput() reads newly produced bytes from the Node bridge process with Process::getIncrementalOutput() / getIncrementalErrorOutput(), but never calls the matching Process::clearOutput() / clearErrorOutput(). Symfony's Process keeps the entire stdout/stderr history in an internal buffer (a php://temp stream that spills to a disk-backed temp file once it exceeds a small in-memory threshold) for as long as the process object is alive — getIncrementalOutput() only tracks a read cursor, it does not discard what was already consumed. On a long-lived Playwright session (a multi-hour Behat regression run, for example) every JSON-RPC response — page HTML, base64 screenshots, ARIA snapshots — accumulates forever in that buffer and is never freed.
Once the temp file grows large enough to fill the available disk space (observed on CI nodes running several thousand scenarios in one process), Process::addOutput() starts silently failing to write further bytes. From that point on, no more bytes are ever visible to getIncrementalOutput(), so every in-flight and subsequent request in ProcessJsonRpcClient::waitForResponse() spins until its deadline and fails with the generic:
Playwright\Exception\TimeoutException: JSON-RPC request N timed out
which looks identical to a slow browser action or a genuine hang, making the real cause (disk full because of an unbounded internal buffer) very hard to diagnose from the PHP side alone.
Reproduction
Not practical to reproduce end-to-end in a short snippet (requires filling disk or running for a long time), but the leak itself is directly observable:
$client = PlaywrightFactory::create();
// ... perform any moderate number of operations that produce output (navigations,
// screenshots, evaluate calls) ...
$process = /* the underlying Symfony Process, via reflection */;
// $process->getOutput() (the *full*, non-incremental buffer) keeps growing without
// bound across the lifetime of the client, even though every chunk was already
// consumed via getIncrementalOutput().
Root cause
src/Transport/JsonRpc/ProcessJsonRpcClient.php, readProcessOutput() (~line 124): calls getIncrementalOutput()/getIncrementalErrorOutput() without the corresponding clearOutput()/clearErrorOutput() calls that Symfony's Process API expects call sites to make when they are done with a chunk.
Proposed fix
private function readProcessOutput(): void
{
$stdout = $this->process->getIncrementalOutput();
+ $this->process->clearOutput();
if ('' !== $stdout) {
$this->outputBuffer .= $stdout;
}
$stderr = $this->process->getIncrementalErrorOutput();
+ $this->process->clearErrorOutput();
if ('' !== $stderr) {
$this->logger->warning('Process stderr', ['stderr' => $stderr]);
}
$this->processAndDispatchMessages();
}
Test
Run several thousand JSON-RPC round trips (or a mocked Process double that returns increasing content from getOutput()), and assert that $process->getOutput()/getErrorOutput() — the full buffers, not the incremental read — stay near-empty rather than growing monotonically with the number of requests served.
Discovered while running the OroCommerce Behat regression suite on this driver: several nodes started failing late in the run with unexplained JSON-RPC request N timed out errors, traced back to full CI-node disks.
A PR with this fix and a regression test is ready and will be linked here.
Affected version: dev-main (2026-07)
Severity: critical — long-running processes eventually fail every request with a misleading timeout
Summary
ProcessJsonRpcClient::readProcessOutput()reads newly produced bytes from the Node bridge process withProcess::getIncrementalOutput()/getIncrementalErrorOutput(), but never calls the matchingProcess::clearOutput()/clearErrorOutput(). Symfony'sProcesskeeps the entire stdout/stderr history in an internal buffer (aphp://tempstream that spills to a disk-backed temp file once it exceeds a small in-memory threshold) for as long as the process object is alive —getIncrementalOutput()only tracks a read cursor, it does not discard what was already consumed. On a long-lived Playwright session (a multi-hour Behat regression run, for example) every JSON-RPC response — page HTML, base64 screenshots, ARIA snapshots — accumulates forever in that buffer and is never freed.Once the temp file grows large enough to fill the available disk space (observed on CI nodes running several thousand scenarios in one process),
Process::addOutput()starts silently failing to write further bytes. From that point on, no more bytes are ever visible togetIncrementalOutput(), so every in-flight and subsequent request inProcessJsonRpcClient::waitForResponse()spins until its deadline and fails with the generic:which looks identical to a slow browser action or a genuine hang, making the real cause (disk full because of an unbounded internal buffer) very hard to diagnose from the PHP side alone.
Reproduction
Not practical to reproduce end-to-end in a short snippet (requires filling disk or running for a long time), but the leak itself is directly observable:
Root cause
src/Transport/JsonRpc/ProcessJsonRpcClient.php,readProcessOutput()(~line 124): callsgetIncrementalOutput()/getIncrementalErrorOutput()without the correspondingclearOutput()/clearErrorOutput()calls that Symfony'sProcessAPI expects call sites to make when they are done with a chunk.Proposed fix
private function readProcessOutput(): void { $stdout = $this->process->getIncrementalOutput(); + $this->process->clearOutput(); if ('' !== $stdout) { $this->outputBuffer .= $stdout; } $stderr = $this->process->getIncrementalErrorOutput(); + $this->process->clearErrorOutput(); if ('' !== $stderr) { $this->logger->warning('Process stderr', ['stderr' => $stderr]); } $this->processAndDispatchMessages(); }Test
Run several thousand JSON-RPC round trips (or a mocked
Processdouble that returns increasing content fromgetOutput()), and assert that$process->getOutput()/getErrorOutput()— the full buffers, not the incremental read — stay near-empty rather than growing monotonically with the number of requests served.Discovered while running the OroCommerce Behat regression suite on this driver: several nodes started failing late in the run with unexplained
JSON-RPC request N timed outerrors, traced back to full CI-node disks.A PR with this fix and a regression test is ready and will be linked here.