Request body is seekable but size is null when a file is sent
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.
I would suggest using POST and overriding the method via override header. PHP does not parse bodies with put requests.
@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.
Would you be able to provide a code sample so we can diagnose the issue? Thanks
Sure, here's a repro https://github.com/BrightSoul/repro-issue159-slim-psr7
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
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();
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.
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
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).