8000 `StreamedResponse` not streaming with swoole integration, when using a `symfony/http-kernel`-based application · Issue #115 · php-runtime/runtime · GitHub
[go: up one dir, main page]

Skip to content
StreamedResponse not streaming with swoole integration, when using a symfony/http-kernel-based application #115
@Ocramius

Description

@Ocramius

While investigating the suitability of runtime/swoole:0.3.0, I ran into a blocker caused by streamed responses.

I was evaluating runtime/swoole because I ran into a problem similar to #50, and as of roadrunner-server/roadrunner#923 (comment), I also couldn't use RoadRunner (which I much prefer to Swoole, at first glance).

A controller producing large, slow responses

Testing this in automation is a bit of a mess (suggestions welcome though), but the idea is that I can stream a response as follows:

final class MyGiantCsvDownloadController
{
    public function someAction(): Response
    {
        return new StreamedResponse(function () {
            foreach (range(1, 5) as $i) {
                echo str_repeat((string) $i, 100000), "\n";
                sleep(1);
            }
        }, Response::HTTP_OK, [
            'Content-Type' => 'text/plain',
        ]);
    }
}

First problem: Symfony\Component\HttpKernel\EventListener\StreamedResponseListener

When interacting with this controller, through runtime/swoole, the respone is not sent via \Runtime\Swoole\SymfonyHttpBridge, where it should happen:

case $sfResponse instanceof StreamedResponse:
ob_start(function ($buffer) use ($response) {
$response->write($buffer);
return '';
});
$sfResponse->sendContent();
ob_end_clean();
$response->end();
break;

In this block, $response->write($buffer); should receive a massive chunk of 111111...22222...<SNIP>...55555, but instead, all output is sent to STDOUT in the worker (not to the response object), and a warning is produced:

<SNIP>55555555555
PHP Warning:  Swoole\Http\Response::write(): data to send is empty in /srv/share/vendor/runtime/swoole/src/SymfonyHttpBridge.php on line 50

Effectively, this write produces nothing, because the response was already sent by symfony/http-kernel:

$response->write($buffer);

After some investigation, I found that the culprit is that symfony/http-kernel sends StreamedResponse through a listener:

https://github.com/symfony/symfony/blob/82e8d23788940421e0ad6e30163242db3ba27a02/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php#L27-L51

I disabled this listener by monkey-patching (while trying out stuff), but if you know a "clean" way to remove it completely from my application, lemme know.

Second problem: response buffer is completely sent in one shot

Assuming we disabled the StreamedResponseListener (clean solution pending), the response content now makes it to Swoole\Http\Response#write($buffer);, but in one big chunk: the HTTP client sees the first byte when swoole finished collecting the whole response.

This is because of this ob_start() call not having a defined buffer size specified:

ob_start(function ($buffer) use ($response) {
$response->write($buffer);
return '';
});

According to the documentation for ob_start():

If the optional parameter chunk_size is passed, the buffer will be flushed after any output call which causes the buffer's length to equal or exceed chunk_size. The default value 0 means that the output function will only be called when the output buffer is closed.

Given that PHP uses a default buffer size of 4096, perhaps it would be a good idea to add one here too? I tried it locally, and my HTTP client starts receiving bytes much earlier than 5 seconds, this way :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0