下载服务器文件时,header() 必须在任何输出前调用;需清除输出缓冲、正确编码中文文件名、使用准确 MIME 类型、分块读取大文件并禁用 readfile()。

下载服务器文件时,header() 必须在任何输出前调用
PHP 输出缓冲机制很敏感:只要 echo、print、空白字符甚至 BOM 字节出现在 header() 之前,就会报 “Headers already sent” 错误,导致下载失败或文件损坏。常见触发点包括:
– 文件开头有 UTF-8 BOM(尤其 Windows 编辑器保存时)
– require 或 include 的配置文件末尾有多余换行
– 开启了 output_buffering 但未显式 ob_end_clean()
推荐做法:在设置头之前加 ob_end_clean(); 清空已有缓冲,并用 headers_sent() 做兜底检查。
Content-Disposition 中文名必须用 filename* 编码
直接写 header('Content-Disposition: attachment; filename="报表.xlsx"'); 在 Chrome/Firefox 可能显示为乱码或截断,因为 filename= 值只允许 ASCII。正确写法是同时提供 ASCII fallback 和 URI 编码的 filename*:
– 先用 rawurlencode() 处理原始名:$encoded = rawurlencode($original_name);
– 再拼成:header("Content-Disposition: attachment; filename=\"{$original_name}\"; filename*=UTF-8''{$encoded}");
注意:UTF-8'' 是固定格式,两个单引号之间不能有空格;filename 和 filename* 并存可兼顾旧版 IE 等客户端。
Content-Type 别硬写 application/octet-stream,优先用真实 MIME 类型
虽然 application/octet-stream 能强制下载,但会丢失语义,且某些安全策略(如企业网关)可能拦截或重命名。更稳妥的方式是动态推断:
– 启用 fileinfo 扩展后用 mime_content_type($path)
– 或根据扩展名查表(如 .pdf → application/pdf,.xlsx → application/vnd.openxmlformats-officedocument.wordprocessingml.document)
– 若不确定且文件较小,再退回到 application/octet-stream
不要依赖 getimagesize() 或 finfo_file() 不带资源释放的写法,避免内存泄漏。
大文件下载必须分块读取,禁用 readfile() 直接加载
readfile() 会把整个文件读进内存再输出,100MB 文件就占 100MB PHP 内存,极易触发 memory_limit 错误或 OOM kill。正确做法是:
– 用 fopen() 打开文件句柄
– fstat() 获取大小并设 Content-Length
– 循环 fread() + echo(每次 8192 字节较稳妥)
– 每次 echo 后调用 flush() 和 ob_flush() 推送数据
– 最后 fclose() 关闭句柄
别忘了在循环前加 set_time_limit(0),防止超时中断。
立即学习“PHP免费学习笔记(深入)”;



















