Skip to content

[Bug]: Browser Testing HttpServer does not send StreamedResponse to the client #1604

Description

@theo-vdml

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

Activity

  1. likemusic commented on Sep 29, 2026

    @likemusic

    This reproduces on the current 5.x (c98e8a5, plugin v5 line), and both halves of the symptom — the UI stuck in isFetching and 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 a StreamedResponse returns false, so the body is captured by buffering it (src/Drivers/LaravelHttpServer.php, around line 365 on 5.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 describes
    

    PHPUnit 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 — a useStream consumer 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, and isFetching never 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 for text/event-stream the trailing \n\n is the event terminator, not whitespace. A body of data: first\n\ndata: second\n\n reaches the browser one event short — EventSource dispatches first and never second. 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's Response already takes ReadableStream|string as its body, so handing it a stream fed by the callback is the shape that would make useStream work 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions