真正流式下载需手动flush并配置全链路:php关缓冲、nginx禁fastcgi_buffering、设正确http头(含range支持与utf-8中文名)、避免内存加载,否则数据滞留缓冲区导致下载失败。

response()->stream() 必须手动 flush,否则数据卡在缓冲区
PHP 默认开启 output buffering,response()->stream() 的回调里只 echo 不 flush(),浏览器就收不到任何字节。常见现象是下载进度条不动、连接超时、或最终只收到空文件。
关键动作不是“读完再发”,而是“读一点、发一点、清一次”。必须组合使用:ob_flush() 清 PHP 缓冲层 + flush() 推送到 Web 服务器。
-
ob_end_clean()要放在回调外(控制器方法开头),干掉 Laravel 中间件可能遗留的输出缓冲 - 每次
fread($handle, 8192)后紧跟echo ...; ob_flush(); flush(); - 加
usleep(1000)防止高频 flush 压垮连接(尤其小 chunk 场景) - Nginx 必须配
fastcgi_buffering off;,否则它会等整个响应结束才转发
大文件别用 Storage::download(),它根本不是流式
Storage::download() 看起来像流式接口,但底层调用的是 response()->download(),而后者对所有磁盘驱动都走 readfile() 或临时落地 —— S3 文件会被先拉到 sys_get_temp_dir() 再吐出,本地大文件也会全 load 进内存。这不是“流”,是“假装流”。
真正流式下载远程文件(如 S3),应避免 Laravel 中转:要么返回 302 重定向到预签名 URL,要么用 stream_wrapper 注册 s3:// 协议后直接 fopen('s3://bucket/key', 'rb')。
- 本地大文件(>50MB):改用
response()->stream()+fopen($path, 'rb') - S3 公有文件:用
redirect()->away($presignedUrl)最轻量 - S3 私有文件且需权限控制:用
response()->stream()+ AWS SDK 的$s3Client->getObject()流式 body,不要->getBody()->getContents()
中文文件名和断点续传不是可选项,是 HTTP 下载的硬需求
不设 Content-Disposition 的 UTF-8 编码格式,Chrome 可能显示为 %E5%B9%B4%E5%BA%A6%E6%8A%A5%E5%91%8A.pdf;不支持 Range 请求,用户网络中断后就得重下整个 GB 日志。
response()->stream() 不自动处理这些,你得自己补全头信息并解析 $_SERVER['HTTP_RANGE']。
- 中文名推荐写法:
'Content-Disposition' => 'attachment; filename*=UTF-8\'\'' . rawurlencode($name) - 断点续传需计算
Content-Range、返回206 Partial Content、跳过文件开头字节再读取 - 务必加
'Accept-Ranges' => 'bytes'和'Content-Transfer-Encoding' => 'binary',否则代理可能破坏二进制流 - 浏览器是否发
Range请求,取决于你是否设置了Content-Length—— 千万别漏掉
streamDownload() 是语法糖,但掩盖了真正的坑
response()->streamDownload() 看似简化了写法,但它只是 response()->stream() 的封装,内部仍依赖你闭包里的输出逻辑。很多人以为写了它就万事大吉,结果忘了关缓冲、没设头、Nginx 没配,最后还是卡死。
它唯一省的事是自动设置 Content-Type 和 Content-Disposition(仅当传了文件名),其他所有流控、错误处理、性能边界都得自己扛。
- 不要在闭包里用
file_get_contents()或Storage::get()—— 这又回到内存加载老路 - PDF/Excel 动态生成时,用
fopen('php://output', 'w')+ 库的output()方法,别echo $pdf->output() - 流式响应期间不能触发 session 写入(如
session()->put()),否则headers already sent - PHP-FPM 下
fastcgi_finish_request()不生效,别指望靠它解耦 flush











