Description
Symfony version(s) affected
6.1.7
Description
Hello. I got OutOfMemroy in BinaryFileResponse in stream_copy_to_stream function.
I try to send partial content with Accept-Range bytes but it doesn't work.
I have memory_limit 256M in php.ini and i have file with size >= 512Mb. When i try to send response
return new BinaryFileResponse('path_to_mp4_video_fil_greater_memory_limit');
I got
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate X bytes) in /var/www/html/vendor/symfony/http-foundation/BinaryFileResponse.php on line 323
What i expect?
I expect that response will use Range headers and will read file by chunkSize. And i see it in source code
// symfony/vendor/symfony/http-foundation/BinaryFileResponse.php
elseif ($request->headers->has('Range') && $request->isMethod('GET')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
if (str_starts_with($range, 'bytes=')) {....
I tried to manually set Content-Type
to video/mp4
and manually set Accept-Range
like this
$r = new BinaryFileResponse($file->filePath);
$r->headers->set('content-type', 'video/mp4');
$r->headers->set('accept-ranges', 'bytes');
// i expected that BinaryFileReponse has own logic to create Content-Range headers and Accept-Ranges based on current memory_limit or something else, but it doesn't
return $r;
but not works for me.
I did't know there is bug or not, but look's like buggy/unexpected behavior. It could use check for memory limit to switch "Range mode" if filesize >= memory_limit. And i think that HTTP-header response to Range header in BinaryFileResponse now incorrect, because it doesn't return Content-Range
to "bytes=0-"
curl -v -H "Range: bytes=0-"
no Content-Range
returned
How to reproduce
First.
bin/console make:controller
Second create/get mp4 file with size > php memory_limit and put it to any not public directory.
Then in controller
return new BinaryFileResponse('path_to_mp4_video_fil_greater_memory_limit');
Possible Solution
I create own custom class for this case
class StreamedFileResponse extends Response
{
private ?Request $request;
private ?int $start;
private ?int $end;
private ?int $chunkSize;
public function prepare(Request $request): static
{
$this->request = $request;
parent::prepare($request);
$this->headers->set('Accept-Ranges', 'bytes');
$this->headers->set('Content-Type', 'video/mp4'); //<!-- FIX TODO:
$this->setStatusCode(206);
$fileSize = filesize($this->content);
$this->headers->set('Content-Length', $fileSize);
$start = 0;
$end = $fileSize - 1;
if($this->request->headers->has('Range'))
{
$range = $this->request->headers->get('Range');
if (str_starts_with($range, 'bytes='))
{
$rangeExp = array_filter(explode('bytes=', $range));
[$start, $end] = explode('-', implode('', $rangeExp));
$start = (int) $start;
$end = (int) $end;
if($start > $end || empty($end))
{
$end = $fileSize - 1;
}
}
$this->headers->set('Content-Range', "bytes {$start}-{$end}/{$fileSize}");
}
$this->start = $start;
$this->end = $end;
$this->chunkSize = $this->start - $this->end <= 0 ? 1024 : $this->start - $this->end;
return $this;
}
/**
* Sends content for the current web response.
*
* @return $this
*/
public function sendContent(): static
{
$fd = fopen($this->content, 'r');
// move to begining
fseek($fd, $this->start);
while(!feof($fd))
{
$content = fread($fd, $this->chunkSize);
echo $content;
flush();
}
fclose($fd);
return $this;
}
}
Additional Context
No response