-
Notifications
You must be signed in to change notification settings - Fork 0
Recipe Streaming Large Files
use InitPHP\HTTP\Message\Response;
use InitPHP\HTTP\Message\Stream;
use InitPHP\HTTP\Emitter\Emitter;
$path = '/var/files/report.pdf';
$response = (new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Length' => (string) filesize($path),
'Content-Disposition' => 'attachment; filename="report.pdf"',
]))->withBody(new Stream(fopen($path, 'rb')));
(new Emitter())->emit($response, /* bufferLength: */ 65536);Three things make this efficient:
- The body
Streamwraps the file's resource handle directly — nofile_get_contents()into memory. - The emitter is given a 64 KiB buffer length, so the file is read and
echo-ed in chunks. - Setting
Content-Lengthexplicitly lets the client show a progress bar and lets reverse proxies enablesendfileoptimisations.
Add it in one block (see also Content-Range):
$total = filesize($path);
$range = $request->getHeaderLine('Range');
$status = 200;
$headers = [
'Content-Type' => 'application/pdf',
'Accept-Ranges' => 'bytes',
];
if (preg_match('/^bytes=(\d+)-(\d*)$/', $range, $m)) {
$first = (int) $m[1];
$last = $m[2] !== '' ? (int) $m[2] : $total - 1;
$status = 206;
$headers['Content-Range'] = sprintf('bytes %d-%d/%d', $first, $last, $total);
$headers['Content-Length'] = (string) ($last - $first + 1);
} else {
$headers['Content-Length'] = (string) $total;
}
$response = (new Response($status, $headers))
->withBody(new Stream(fopen($path, 'rb')));
(new Emitter())->emit($response, 65536);Accept-Ranges: bytes on the first 200 response tells the client it can negotiate ranges next time around — important for <video> and <audio> seeking.
Don't (string) $response->getBody() it into memory; read it incrementally:
$client = new \InitPHP\HTTP\Client\Client();
$response = $client->sendRequest(new \InitPHP\HTTP\Message\Request('GET', $url));
$out = fopen('/tmp/big.bin', 'wb');
$body = $response->getBody();
while (!$body->eof()) {
fwrite($out, $body->read(65536));
}
fclose($out);The PSR-18 response body in this package is backed by php://temp (small payloads stay in memory; larger ones spill to disk), so even before you start reading you're not paying RAM for the whole response.
The Client's 30 s default timeout includes the response body transfer. For multi-GB downloads, raise it — or disable it for that specific client instance:
$client = (new \InitPHP\HTTP\Client\Client())
->withTimeout(0) // no overall timeout
->withConnectTimeout(10); // but still fail fast on connectionSSE looks like a streaming HTTP response but isn't a great fit for the PSR-7 emitter abstraction — the emitter's chunked path assumes a bounded body that ends eventually. For genuinely unbounded push streams (SSE, log tail, queue consumers), echo the frames yourself and flush() directly inside your event loop, with reverse-proxy buffering turned off:
location /events {
proxy_buffering off;
gzip off;
}-
Emitter — Chunked Bodies —
bufferLengthtuning. - Content-Range — partial-content emission.
- Recipe — File Upload — the mirror direction.
initphp/http · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
PSR-7 Messages
PSR-17 Factories
PSR-18 Client
Emitter (SAPI)
Static Facades
Recipes
Reference
Migration & Help