guzzle / guzzle

Guzzle, an extensible PHP HTTP client

Home Page:https://docs.guzzlephp.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using `"stream" => true` in options makes PSR7 responses read-once

garethellis36 opened this issue · comments

Guzzle version(s) affected: 7.5.0
PHP version: 8.1.21
cURL version: 7.61.1

Description
We have enabled streaming of responses as per this Request Option.

While debugging another problem, I enabled the log middleware, and configured it to write the full response body to the log. I discovered that the response body could not be read a second time, because the StreamInterface implementation is not seekable. I believe that this is because StreamHandler uses fopen() to create a stream wrapper, and HTTP stream wrappers are inherently not seekable - however I do admit that I do not completely understand the mechanics of this. I spent quite a long time reading Guzzle source code trying to figure out how everything fits together.

How to reproduce

$handlerStack = HandlerStack::create();
$handlerStack->push(Middleware::log($logger, new MessageFormatter(MessageFormatter::DEBUG)));

$options = ['handler' => $handlerStack];
$client = new \GuzzleHttp\Client($options);

$response = $client->get($url, ['stream' => true]);
$body = (string)$response->getBody();
// `$body` is an empty string because the stream has already been fully read by the log middleware, and it can't be rewound

Additional context
If my understanding is correct, I do not think there is a solution for this, it's an inherent drawback of streaming responses. If this is the case then I think a documentation update would be useful because I spent a lot of time debugging this and trying to understand why the responses were read-once.

We are using stream here so that we can download large files and return the remote HTTP response directly from our application as another stream, without having to load the entire response into memory.

You should disable the stream option. It doesn't solve the problem you are asking about. We already buffer things to disk when it exceeds some threshold which I don't remember (something like 1MB).

Thank you; out of interest, can you point me towards the code which buffers to disk so I can understand how this works?

If you're using ext-curl, this happens here:

$options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
. The threshold where by we move from in-memory to on disk depends on PHP config, the default being 2MB according to https://www.php.net/manual/en/wrappers.php.php.