Slim-Psr7 icon indicating copy to clipboard operation
Slim-Psr7 copied to clipboard

Request body is seekable but size is null when a file is sent

Open BrightSoul opened this issue 5 years ago • 4 comments

Hello there, I'm using Slim 4.5.0. I'm PUTting a binary file to one of my routes but the Slim\Psr7\Stream instance I get from the Slim\Http\ServerRequest object won't tell me the content size. I'm NOT sending a multipart/form-data request. I'm just PUTting a file with curl -T "filename" "http://my-host/my-route"

Here's my PHP code. $request->getBody()->getSize() is null even though the stream is reported seekable and the Content-Length header was provided.

public function putMediaContent(ServerRequestInterface $request, ResponseInterface $response, array $args) : ResponseInterface {
    //body is a Slim\Psr7\Stream instance
    $body = $request->getBody();
    //size is null
    $size = $body->getSize();
    //isSeekable is true
    $isSeekable = $body->isSeekable();
    //contentLength has the correct uploaded file size
    $contentLength = $request->getHeaders()['Content-Length'][0];

    //...
}

This is a problem since I then have to pass this stream to an S3Client for cloud upload and it complains about the fact the size is unknown. Is this intended?

Thanks.

BrightSoul avatar May 28 '20 14:05 BrightSoul

I would suggest using POST and overriding the method via override header. PHP does not parse bodies with put requests.

l0gicgate avatar May 28 '20 15:05 l0gicgate

@l0gicgate Thanks for your quick reply. Unfortunately, POST is not working either. As a workaround, I wrote a StreamWrapper class which returns the size I got from the Content-Length header. And for the rest, it just relays calls to the underlying stream.

BrightSoul avatar May 28 '20 15:05 BrightSoul

Would you be able to provide a code sample so we can diagnose the issue? Thanks

l0gicgate avatar May 28 '20 16:05 l0gicgate

Sure, here's a repro https://github.com/BrightSoul/repro-issue159-slim-psr7

BrightSoul avatar May 28 '20 16:05 BrightSoul

I can confirm this behavior.

Minimal app:

<?php

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\UriInterface;
use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->put('/upload', function (ServerRequestInterface $request, ResponseInterface $response) {
    $size = $request->getBody()->getSize() ?? 'null';
    $contentLength = $request->getHeader('Content-Length')[0];
    
    $result = "Content-Length: $contentLength, Body size: $size";
    $response->getBody()->write($result);

    return $response;
});

$app->run();

Actual result:

Content-Length: 566967, Body size: null

I have tested this with slim/psr7 and nyholm/psr7. The result was the same in both cases. So I would consider this not as a bug, because a PUT stream can only be read once. So any change could break the compatibility.

A workaround for this specific case would be a custom middleware:

slim/psr7 example:

final class PutUploadMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if ($request->getMethod() === 'PUT') {
            $streamFactory = new \Slim\Psr7\Factory\StreamFactory();
            $request = $request->withBody($streamFactory->createStream(file_get_contents('php://input')));
        }
        return $handler->handle($request);
    }
}

Nyholm example:

use Nyholm\Psr7\Stream;
// ...

final class PutUploadMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if ($request->getMethod() === 'PUT') {
            $request = $request->withBody(Stream::create(file_get_contents('php://input')));
        }
        return $handler->handle($request);
    }
}

Closing this as resolved.

odan avatar Jul 08 '24 20:07 odan

@odan

So I would consider this not as a bug, because a PUT stream can only be read once.

How is the stream being read? Please consider reopening this issue.

I found that the problem comes from fstat($this->stream) === false here:

https://github.com/slimphp/Slim-Psr7/blob/753e9646def5ff4db1a06e5cf4ef539bfd30f467/src/Stream.php#L183-L194

Workarounds I founs so far:

$stream = $request->getBody();

// Get content length (strlen is binary safe)
$size = strlen($stream->getContents());

// Create new via php://temp
$size = (new \Slim\Psr7\Factory\StreamFactory())->createStream($stream->getContents())->getSize();

piotr-cz avatar Oct 09 '24 12:10 piotr-cz

Additionally, there is already a workaround in place, just it doesn't have effect in this case:

https://github.com/slimphp/Slim-Psr7/blob/753e9646def5ff4db1a06e5cf4ef539bfd30f467/src/Factory/ServerRequestFactory.php#L87-L94

IMHO reproduction with nyholm/psr7 (https://github.com/Nyholm/psr7/issues/254) doesn't mean this is not a bug, rather that both libraries have same issue.

piotr-cz avatar Oct 10 '24 08:10 piotr-cz

Hi @piotr-cz

I understand that this can be frustrating, that's why we already have tried different approaches to fix this in the last years. For instance by caching the stream. See this PR: https://github.com/slimphp/Slim-Psr7/pull/124

(https://github.com/Nyholm/psr7/issues/254) doesn't mean this is not a bug, rather that both libraries have same issue.

This "special" behavior originates from PHP's handling of php://input, especially in PUT requests. Once the input is consumed, it cannot be read again because it's a non-rewindable stream.

As already mentioned there a some workarounds from the community, like a custom StreamWrapper or the PutUploadMiddleware. Have you tried / tested that?

odan avatar Oct 10 '24 11:10 odan

@odan

Thank you, I'm already using one of the workaround: strlen((string) $stream);.

In this case I think this special behavior could be noted somewhere in docs page https://www.slimframework.com/docs/v4/ so every developer that tries it, doesn't have to find out about it the hard way.

By thy way, issue is not only for PUT but also POST requests (probably all methods except GET that doesn't allow payload body).

piotr-cz avatar Oct 10 '24 12:10 piotr-cz