Skip to content

Send partial content is not possible with BinaryFileResponse with memory_limit less than filesize #48692

Closed
@ivanbogomoloff

Description

@ivanbogomoloff

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions