浏览器正确触发下载的关键是设置Content-Type为application/octet-stream和Content-Disposition为attachment; filename="xxx",并确保无输出前发送响应头、路径安全校验及Web服务器正确路由请求。
浏览器能否正确触发下载,关键不在 PHP 是否读取了文件,而在于响应头是否明确告诉浏览器“这不是要渲染的页面,而是要保存的文件”。Content-Type 设为 application/octet-stream 是最稳妥的选择——它不依赖文件扩展名,避免因 MIME 类型识别错误导致浏览器尝试打开而非下载。同时必须设置 Content-Disposition 为 attachment; filename="xxx",其中 filename 值需用 rawurlencode() 处理中文名,否则 Safari 和部分安卓浏览器会乱码或截断。
Content-Type: application/octet-stream 比 application/pdf 或 image/png 更通用,尤其适合用户上传后又下载的场景Content-Disposition: attachment; filename*=UTF-8''xxx.pdf 是 RFC 5987 标准写法,兼容性优于仅用 filename
header() 之前确保无任何输出(包括空格、BOM、echo),否则 headers already sent 错误直接中断流程三者都能输出文件内容,但适用场景和资源消耗差异明显。readfile() 最简单,适合中小文件(一般 ≤ 10MB),它自动处理缓冲并返回字节数;fpassthru() 需要先 fopen(),优势在于可配合 fseek() 实现断点续传或分片下载;而手动 fread() 分块读取只在需要加密、动态拼接或限速时才值得引入——多数下载功能没必要自己管理缓冲区。
readfile($filepath):代码短、不易出错、PHP 内部优化好fopen() + fpassthru(),并手动解析 $_SERVER['HTTP_RANGE']
file_get_contents() 加 echo 下载大文件——内存会爆,且无法控制传输过程直接拼接 $_GET['file'] 到路径里是典型路径遍历漏洞。比如传入 ../../etc/passwd 就可能泄露系统文件。核心原则是:不信任任何用户输入,路径必须白名单校验或严格限制根目录。
basename($_GET['file']) 只取文件名,再拼到预设的下载目录(如 /var/www/uploa
ds/),彻底切断上级路径可能realpath() + strpos() 检查结果是否仍位于允许目录内,例如:if (0 !== strpos(realpath($user_path), '/var/www/uploads')) die('Access denied');
is_file()、is_readable())必须在路径规范化之后执行这类问题往往不是 PHP 逻辑错,而是 Web 服务器配置或 URL 路由干扰所致。Nginx/Apache 若把下载请求当成普通页面路由,就可能转发给 index.php,导致 PHP 脚本没运行或返回 HTML 内容;或者 .htaccess 中的重写规则意外截断了带参数的 URL。
download.php?file=test.pdf 被重写成 /download/test.pdf 后,实际未映射回 PHP 脚本.php 后缀的请求确实交由 PHP 解释器执行,某些共享主机禁用了 query string 传递text/html 或 404,说明请求根本没进到你的 PHP 文件里