When using the maennchen/zipstream-php library in a Laravel application, you may encounter the following error:

Cannot modify header information - headers already sent by (output started at .../ZipStream.php:808)

This happens because ZipStream writes directly to the output buffer and sends headers as it streams the ZIP content. If Laravel attempts to send its own response headers afterward, the result is a conflict that PHP raises as an error.

Laravel typically buffers output and sends headers just before the response is returned. However, ZipStream bypasses Laravel’s response lifecycle and begins output immediately—before Laravel has had a chance to send its headers. As a result, Laravel’s later call to header() fails because output has already been sent.

To avoid this issue, wrap your ZipStream logic in a StreamedResponse. This tells Laravel to hand off control of the response entirely, including headers and output.

use ZipStream\ZipStream;
use Symfony\Component\HttpFoundation\StreamedResponse;

public function downloadDocuments()
{
    return new StreamedResponse(function () {
        $options = new \ZipStream\Option\Archive();
        $options->setSendHttpHeaders(true); // let ZipStream send headers

        $zip = new ZipStream(null, $options);
        $zip->addFile('example.txt', 'This is the content of the file.');
        $zip->finish();
    });
}

Key takeaways

  • ZipStream outputs content and headers directly—before Laravel can send its own.
  • Wrapping it in StreamedResponse avoids Laravel interfering with the output buffer.
  • Never return a standard response() or Response object when using ZipStream.

If you stick to StreamedResponse, Laravel will not try to send any additional headers, and the error will be resolved.