Yii框架默认无法直接下载大文件,因StreamResponse被中间件和PHP输出缓冲拦截导致内存全载入;需禁用output_buffering、fastcgi_buffering及gzip,手动设置Content-Length与Content-Disposition,并在流回调中fread+echo+flush+connection_status检查。

Yii 框架默认不能直接下载大文件,因为 StreamResponse 会被中间件和 PHP 输出缓冲拦截,变成内存全载入——这不是“不支持流式”,而是配置没绕过默认响应链。
为什么 return new StreamResponse() 没反应?
常见现象:浏览器卡住、返回空响应、Nginx 报 502、PHP 内存溢出。根本原因是 Yii 的 Response 组件在 send() 阶段会自动调用 setContentLength() 并收集全部输出,而 StreamResponse 依赖的是 php://output 直写,两者冲突。
- 控制器里只调用
$response->send()不行,必须return $response,让其成为最终响应对象 - 不能在
beforeAction、过滤器或日志中提前写任何输出(包括echo、var_dump、Yii::info()) -
ob_start()或output_buffering = 4096(php.ini)必须禁用,否则流被截在缓冲区 - 确保没启用
yii\log\FileTarget等会在响应过程中写磁盘的日志组件
怎么写一个真正边读边发的下载逻辑?
核心是把文件读取和 echo + flush() 封进 callback,并手动设 Content-Length 和 Content-Disposition。不要依赖 StreamResponse 的构造参数自动推断。
- 用
fopen($filePath, 'rb')而非file_get_contents(),避免内存加载 - 每次
fread($fp, 8192)后必须跟echo+flush();仅flush()不够,Web 服务器(如 Nginx)可能还缓存 - 务必手动计算并设置
Content-Length:$size = filesize($filePath); $response->headers->set('Content-Length', $size) - 中文文件名需编码:
rawurlencode($filename)或用mb_convert_encoding($filename, 'UTF-8', 'auto')后再rawurlencode()
return Yii::$app->response->stream(function () use ($filePath, $fileName) {
$fp = fopen($filePath, 'rb');
if (!$fp) {
throw new NotFoundHttpException('File not found');
}
while (!feof($fp)) {
echo fread($fp, 8192);
if (connection_status() !== CONNECTION_NORMAL) {
break;
}
flush();
}
fclose($fp);
}, [
'contentType' => 'application/octet-stream',
'contentDisposition' => 'attachment; filename="' . rawurlencode($fileName) . '"',
]);
Nginx 和 PHP-FPM 有哪些关键配置要关?
即使 PHP 层写对了,Nginx 或 PHP-FPM 仍可能吞掉流式响应。这些不是可选项,是必改项。
- Nginx:在 location ~ \.php$ 块中加
fastcgi_buffering off;(注意不是fastcgi_buffers) - PHP-FPM:在 pool 配置里设
php_flag[output_buffering] = Off,或统一在 php.ini 中设output_buffering = Off - PHP-FPM 还要检查
request_terminate_timeout,它比set_time_limit(0)更优先;建议设为300s以上,视最大文件传输时间而定 - 如果用了
mod_deflate(Apache)或 gzip on(Nginx),必须关闭——压缩需要全文本,与流式互斥
连接中断时文件为啥总不完整?
流式传输没有回滚机制。用户点取消、网络闪断、手机切后台,服务端不会立刻感知,还在傻读文件,直到 fclose() 才结束。这会导致资源浪费甚至句柄泄漏。
- 每读一段后加
if (connection_status() !== CONNECTION_NORMAL) { return; } - 不要设
ignore_user_abort(true),保持默认false,让 PHP 在断连后能退出 - 避免在流 callback 里调用
file_put_contents或触发任何日志写入,它们可能隐式调用flush()干扰状态判断 - 别用
X-Sendfile或X-Accel-Redirect和 StreamResponse 混用——前者是 Web 服务器直读,后者是 PHP 流控,路径完全不同
最易忽略的一点:所有 header 设置(包括 Content-Length、Content-Disposition)必须在 stream() callback 执行前完成;一旦 callback 开始输出,header 就不能再改,否则报 headers already sent 错误。


















