Repository navigation
[Bug]: Browser Testing HttpServer does not send StreamedResponse to the client #1604
Description
Activity
This reproduces on the current
5.x(c98e8a5, plugin v5 line), and both halves of the symptom — the UI stuck inisFetchingand the payload appearing in the terminal — come from the same six lines. Measurements below, WSL2 (Linux 6.18 kernel on a Windows host), PHP 8.4.24, Playwright 1.62.1.Why the data lands in the terminal
LaravelHttpServer::handleRequest()asks the response for its content, and aStreamedResponsereturnsfalse, so the body is captured by buffering it (src/Drivers/LaravelHttpServer.php, around line 365 on5.x):$content = $response->getContent(); if ($content === false) { try { ob_start(); $response->sendContent(); } finally { $content = mb_trim(ob_get_clean()); } } return new Response($response->getStatusCode(), $response->headers->all(), $content);
Streaming code flushes — that is what makes it streaming.
ob_flush(); flush();inside the callback flushes this buffer outward, to the process's own stdout, which is the terminal running Pest.ob_get_clean()afterwards holds only whatever was echoed since the last flush, so with every chunk flushed it holds nothing.Measured, a route echoing three chunks with
ob_flush(); flush();after each, fetched by the page:browser received: "" chunk1 chunk2 chunk3 <- printed by the test run, exactly as this issue describesPHPUnit notices it too and marks the test risky: "Test code or tested code printed unexpected output: chunk1 chunk2 chunk3". The same route without the flushes delivers its body to the browser normally, which isolates the cause to the flush rather than to streaming or to the route.
Why the UI never updates even when the body does arrive
The body is assembled in full before
new Response(...)is constructed, so there is no incremental delivery in this design at all — auseStreamconsumer cannot receive a first chunk before the last one exists. Without the flushes the fetch at least completes; with them it completes with an empty body, andisFetchingnever clears.Two fixes, one of them small
The trailing terminator. Even in the non-flushing case the body is altered:
mb_trim()strips edge whitespace, and fortext/event-streamthe trailing\n\nis the event terminator, not whitespace. A body ofdata: first\n\ndata: second\n\nreaches the browser one event short —EventSourcedispatchesfirstand neversecond. That part is a two-line change, open as pestphp/pest-plugin-browser#267, with a test for exactly this.The flush escaping the buffer is the larger half and #267 does not address it.
ob_start()accepts a callback, which would keep flushed chunks instead of letting them reach stdout; and amphp'sResponsealready takesReadableStream|stringas its body, so handing it a stream fed by the callback is the shape that would makeuseStreamwork as intended rather than merely stop leaking. I have not built either, so I am not claiming a patch here — but the leak alone is worth separating from the streaming question, since it corrupts unrelated test output.Happy to test a patch on this setup.
What Happened
When using Laravel's useStream composable (@laravel/stream-vue) with Pest Browser Testing, the streamed data bypasses the browser UI completely and is output directly to the terminal console where the Pest test is running.
Expected behavior: The UI updates incrementally as data streams from the server.
Actual behavior: The stream freezes in the isFetching state, the UI never updates, and the streamed data appears in the terminal console instead.
How to Reproduce
See the demo in the Sample Repository
Sample Repository
https://github.com/theo-vdml/pest-browser-use-stream/
Pest Version
4.3.1
PHP Version
8.4.16
Operation System
Linux
Notes
No response