必须手动设置content-disposition响应头,否则浏览器直接渲染而非下载;常见原因包括nginx默认过滤该头、thinkphp中间件覆盖、中文文件名未按rfc5987格式编码(filename*=utf-8'')、大文件未用stream分块导致内存溢出、上传文件直链暴露安全风险等。

必须手动设置 Content-Disposition 响应头,否则浏览器不会下载,而是直接渲染内容(比如显示 JSON、HTML 或乱码)。
为什么设了还是不下载?常见 header 被覆盖或丢失
ThinkPHP 的中间件、Nginx 或 CDN 都可能删掉或重写 Content-Disposition。尤其 Nginx 默认会过滤非标准响应头,proxy_pass 后若没显式开启 underscores_in_headers on 或用 add_header 强制透传,该头就没了。
- 检查响应中是否真有
Content-Disposition:用浏览器 DevTools → Network → Response Headers 查看 - Nginx 配置里,在
location ~ \.php$块中加:fastcgi_hide_header Content-Disposition;这句必须删掉(它会主动屏蔽该头) - 改用
add_header Content-Disposition $sent_http_content_disposition always;确保透传 - ThinkPHP 中若用了
Response::create()->header(),注意不要被后续中间件覆盖;推荐用return response()->download()或Response::stream()封装
中文文件名乱码?别用 rawurlencode() 单独套一层
rawurlencode() 只生成 URL 编码部分,但完整 Content-Disposition 必须符合 RFC5987,格式是 filename*=UTF-8''%E8%AE%A2%E5%8D%95.xlsx —— 注意中间有两个单引号,且前面不能有空格或引号包裹。
- 错误写法:
'filename="' . rawurlencode($name) . '"'→ 浏览器识别为 ASCII 名,解码失败 - 正确写法:
'filename*=UTF-8\'\''. rawurlencode($name)(注意两个单引号是字面量) - 封装成函数更安全:
function encodeFilename(string $filename): string { return 'filename*=UTF-8\'\''. rawurlencode($filename); } - 传给
download()时作为第三个参数:['Content-Disposition' => 'attachment; ' . encodeFilename('订单-2024.xlsx')]
大文件下载卡死或内存溢出?必须用 stream + flush
直接 readfile() 加大文件,PHP 会把整个文件读进内存再输出,容易触发 OOM;Response::download() 内部也是这么干的,不适合 >100MB 场景。
- 改用
Response::stream(),手动分块读取:fread($fp, 8192)+echo+flush()+ob_flush() - 务必设置
Content-Length头,否则 Safari 和部分安卓浏览器无法显示进度条 - 关闭输出缓冲:
if (ob_get_level()) ob_end_clean();放在 stream 函数开头 - 避免使用
exit或die中断流,会导致连接提前关闭
上传文件下载必须走 PHP 控制,禁止直链
上传目录如果放在 public/ 下(如 public/uploads/),用户拼出 URL 就能直接访问,等于开放路径遍历入口。哪怕加了 .htaccess,Nginx 也不认这个。
- 上传路径必须移出 Web 根目录,例如
runtime/uploads/或storage/uploads/ - 下载逻辑必须由 PHP 接口控制:
readfile()输出 + 手动 setHeader,不能返回/runtime/uploads/xxx.pdf让前端自己 fetch - 下载前校验文件是否存在、是否属于当前用户、扩展名是否在白名单内(别信
$_FILES['type']) - 临时文件下载完不自动清理?得在
stream回调末尾加unlink($path),但要确保流已结束且无并发读取
最容易被忽略的是 Nginx 对 Content-Disposition 的静默丢弃,以及中文名漏写 filename*=UTF-8'' 中间那两个单引号 —— 少一个,IE 和旧版 Edge 就当没这回事。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











