HTTP响应必须严格按“状态行→响应头→空行→响应体”顺序发送,PHP一旦输出body内容(含空格、BOM等)即强制发送header,后续header()调用失效;启用output buffering可延迟实际发送,但缓冲区满或显式刷新后仍会触发headers already sent错误。

HTTP协议规定header必须在body之前发送
根本原因不在PHP本身,而在HTTP协议设计:一个合法的HTTP响应必须严格按“状态行 → 响应头 → 空行 → 响应体”顺序组织。一旦PHP开始向客户端写入任何body内容(哪怕是一个空格、换行或<html>),Web服务器就认为header阶段已结束,后续再调用header()就等于试图往已封包的响应里塞新header——这违反协议,被直接拒绝。
浏览器收到的响应若开头不是HTTP/1.1 200 OK或HTTP/1.1 302 Found这类状态行,而是<html>或空白,说明header早已被强制发出,header("Location: ...")自然失效。
为什么有时候“有输出也能跳转”?
这其实是输出缓冲(output buffering)在起作用,并非协议允许,而是PHP帮你“延迟了实际发送”。
- 当
output_buffering开启(如php.ini设为4096)或脚本开头调用了ob_start(),PHP会把所有echo、HTML等暂存内存,不立刻发给Web服务器 - 此时
header()仍能修改待发送的header列表,直到脚本结束或显式调用ob_end_flush()才真正输出 - 但缓冲区满(比如echo了5000字节而buffer只设4096)、或提前调用
ob_flush()/ob_end_clean(),就会立刻触发header发送,之后再header()必报headers already sent
常见“隐形输出”来源和检测方法
很多问题不是你写了echo,而是编辑器、include文件或编码惹的祸:
立即学习“PHP免费学习笔记(深入)”;
- UTF-8 BOM头:文件开头的
EF BB BF字节会被当成普通输出,用编辑器(如VS Code)切换“UTF-8 without BOM”保存 -
?>后多出的空格或换行:尤其是被include的配置文件末尾,肉眼难见 - 错误日志或调试语句残留:比如
var_dump($x);没删干净,哪怕放在if (false)里也会执行 - 用
headers_sent($file, $line)准确定位:它返回true时,$file和$line就是第一个输出发生的位置
Location头格式错误也会导致跳转静默失败
即使header没被提前发送,格式错也得不到302重定向效果,浏览器只会当普通200响应处理:
-
Location冒号后**必须有且仅有一个空格**:header("Location: https://example.com")✅;header("Location:https://example.com")❌(部分服务器容忍,但不保证) - URL必须是绝对路径:
header("Location: /login.php")在某些SAPI(如CLI SAPI)下无效,应写全协议+域名 - 跳转后务必
exit或die:否则后续代码(比如数据库操作、敏感输出)仍会执行并可能污染响应体
真正棘手的是:header没报错、浏览器也没提示,页面却卡在原地——这时要抓包看响应状态码是不是200而不是302,再回头查Location头是否拼错或被截断。



















