最常用方式是response()->download(),自动设置content-disposition: attachment头触发下载,要求传入服务器本地绝对路径(如storage_path()或public_path()),不支持url或相对路径;动态内容应使用streamdownload()配合闭包流式输出。

用 response()->download() 返回文件下载
直接返回文件下载最常用的方式就是 response()->download(),它会自动设置 Content-Disposition: attachment 头,触发浏览器下载行为。注意它底层调用的是 readfile(),所以文件路径必须是服务器本地可读的绝对路径(不能是 URL)。
常见错误:传入相对路径或 public 下的 URL 路径(比如 public/uploads/report.pdf 或 https://xxx.com/uploads/report.pdf),这会导致 404 或 “File not found” 错误。
- 正确写法:
response()->download(storage_path('app/reports/2024-report.pdf')) - 如果文件在
public/目录下,需用public_path()构造绝对路径:response()->download(public_path('files/manual.zip')) - 支持自定义文件名:
response()->download($path, '用户手册-v2.1.pdf') - 不建议在控制器里拼接
$_SERVER['DOCUMENT_ROOT']或硬编码路径,易出错且不可移植
用 response()->streamDownload() 动态生成文件
当文件内容不落地(比如导出 CSV、动态 PDF、加密后临时流),就不能用 download() —— 它只认磁盘文件。这时该用 streamDownload(),传一个闭包,由你控制数据怎么“一边生成一边吐给响应流”。
典型场景:导出数据库查询结果为 CSV,但数据量大,不想先写到临时文件再读;或者用 Dompdf 渲染 HTML 后不存盘直接下载。
- 闭包内必须调用
fputcsv()、echo或ob_flush()等输出动作,否则响应为空 - 记得手动设置
Content-Type和Content-Disposition(streamDownload()不自动设attachment) - 示例:
response()->streamDownload(fn () => fputcsv(STDOUT, ['id', 'name']), 'users.csv') - 注意内存和超时:大文件流式处理时,避免在闭包里做全量
collect()或get(),应配合chunkById()
为什么不用 return redirect() 或 return view() 实现下载
有人试图用重定向跳转到一个 /download/{id} 的 GET 路由来“间接下载”,或者在视图里放个 <a href="/file.pdf"></a> 链接——这些方式都不安全也不可控。
问题很实际:无法校验权限(比如用户是否能下载这个 ID 的文件)、无法记录下载日志、无法动态修改文件名、无法限制下载次数或过期时间。更严重的是,直接暴露文件 URL 可能绕过 Laravel 的中间件保护。
- 所有下载逻辑必须走控制器方法,并放在需要鉴权的中间件组里(如
auth、can:download,App\Models\File) - 不要把
storage/app/目录直接配置成 Web 可访问路径(哪怕加了.htaccess),这是安全隐患 - 如果真要前端触发,用
axios.get('/api/download/123', { responseType: 'blob' })配合 Blob URL,而不是window.location.href
下载大文件时要注意响应超时和内存
Laravel 默认使用 PHP-FPM,而 FPM 的 request_terminate_timeout 和 Nginx 的 fastcgi_read_timeout 都可能中断大文件传输。这不是代码写错了,是配置没跟上。
典型现象:下载 50MB 文件时卡在 80%,然后报 504 Gateway Timeout 或连接被重置。
- PHP 层:在下载逻辑前加
set_time_limit(0)(仅限 CLI/FPM 场景,不适用于 Swoole) - Nginx 层:确保
fastcgi_read_timeout和proxy_read_timeout(如用了反向代理)设为足够大值(如 600) - 避免用
Storage::get()读大文件进内存,改用Storage::readStream()+response()->stream()组合 - 对于超大文件(>500MB),考虑改用预签名 URL(如 S3)或分片下载方案,别硬扛在 Laravel 响应里
真正难的不是写出那行 response()->download(),而是想清楚文件从哪来、权限怎么控、超时怎么扛、日志怎么记——这些细节漏掉一个,上线后就容易半夜收告警。











