skip to Main Content

Update
This seems to relate in some way to the reading of the stream when outputting. The function used by Slim to output the body looks like this, where $body implements StreamInterface and $this->responseChunkSize is 4096:

$amountToRead = $body->getSize();
while ($amountToRead > 0 && !$body->eof()) {
    $length = min($this->responseChunkSize, $amountToRead);
    $data = $body->read($length);
    echo $data;

    $amountToRead -= strlen($data);

    if (connection_status() !== CONNECTION_NORMAL) {
        break;
    }
}

It appears the $body->eof() call (which is just a wrapper for PHP’s feof() function) is returning true even though the full file has not been read. Not sure why that would be though. I also verified that this does not occur if I just do an fopen() on the file and create a Stream from it, then run the same code. It only happens when the stream is the product of the external REST API call via Guzzle.

Original Post
I have a service built using Slim (v4.4) that calls an external REST API using Guzzle (v6.5.3) that returns a file. This is running in Windows, web server is IIS/FastCGI (I know, unusual). PHP version is 7.3.10. The call from Slim to the external REST API retrieves the file just fine, but when my app calls the service, some files get corrupted, seems some data gets lost based on what I see in the file size. The call from the service to the external REST API is fairly simple:

$file_response = $guzzleClient->request('GET', "{$base_url}/docs/{$file_id}", [
    'headers'   => [
        'Authorization' => "token {$token}"
    ]
]);

The above call works fine and returns the file correctly, I can either display it to screen or use the ‘sink’ option in Guzzle to save to a file, it works fine. But when I try to call the service that wraps that call, it fails. I tried a couple things. Firstly, I was just returning the response as is since it conforms to the interface required anyway. My Slim route looks like this:

$app->group('/files', function (Group $group) {
    $group->get('/{file_id}', GetFileAction::class);
});

The GetFileAction class has a method like this:

public function __invoke(Request $request, Response $response, $args): Response {
    ...Guzzle request returning $file_response here...
    return $file_response;
}

My app is also using Guzzle to call the service, the call looks like this:

$guzzleClient->request(
    'GET',
    "{$base_url}/files/{$file_id}",
    [
        'auth' => [$username, $password],
        'sink' => $file_path
    ]
);

I wondered if returning the Guzzle response in Slim might be causing some unexpected result, so I tried returning this in the service instead:

return $response->withBody(new SlimPsr7Stream($file_response->getBody()->detach()));

Same result. Obviously if somebody who has run into this exact same problem can help out it would be great, but if not some pointers on how I could try to debug the handling of the streams would likely be helpful.

2

Answers


  1. Chosen as BEST ANSWER

    I've confirmed this is linked to a weird issue with the feof() function returning true even though it hasn't read the full file. The solution I came up with involved creating a different Response Emitter than the default Slim 4 one (mostly the same) and overwrite the emitBody function so it does not rely on feof(). I did so like this:

    $length = min($this->responseChunkSizeCopy, $amountToRead);
    while ($amountToRead > 0 && ($data = $body->read($length)) !== false) {
        echo $data;
        $amountToRead -= $length;
        $length = min($this->responseChunkSizeCopy, $amountToRead);
    
        if (connection_status() !== CONNECTION_NORMAL) {
            break;
        }
    }
    

    So far this has worked well based on my testing. I have no idea why feof() is not working as expected and didn't really find anything that seemed to specifically address it. Maybe it's a Windows specific thing, and since PHP is less common on Windows it's not a common occurrence. But leaving this solution here in case it can help someone.


  2. I’m trying to achieve a similar goal—using Slim to proxy and forward incoming requests to another service via a Guzzle client—and encountered a similar problem when returning the Guzzle response.

    In my case the problem was that the other service was incorrectly returning a Transfer-Encoding: chunked header in the response.

    Your mileage may vary, but the solution was to replace this with a correct Content-Length header in the returned response:

        return $response
            ->withoutHeader('Transfer-Encoding')
            ->withHeader('Content-Length', $response->getBody()->getSize());
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search