header() 报“headers already sent”错误是因为HTTP响应头必须在任何输出(HTML、空格、BOM、echo等)前发送;常见原因包括UTF-8 BOM、文件开头空行、提前输出,解决需清除BOM、避免前置输出,或临时启用ob_start()但须根治源头。

为什么 header() 会报 “headers already sent” 错误
PHP 的 header() 函数必须在任何实际输出(HTML、空格、换行、echo、print、甚至 BOM 字节)发送到浏览器前调用。一旦输出缓冲区被冲刷(比如脚本开头有空行、UTF-8 BOM、或提前 echo "test"),HTTP 响应头就已发出,再调用 header() 就会触发这个错误。
常见诱因包括:
- 文件开头存在 UTF-8 BOM(尤其 Windows 编辑器保存时默认加 BOM)
?><?php 之间有空格或换行(比如模板文件末尾多了一个回车)
- 包含的配置文件、函数库中意外输出了空格或调试语句(如
echo "loaded";)
-
error_reporting(E_ALL) 开启后,未捕获的 Notice/Warning 也会作为文本输出,破坏 header 时机
怎么快速定位哪一行先输出了内容
PHP 错误信息末尾通常会提示类似:in /path/to/file.php on line 12——但这只是 header() 被调用的位置,不是“输出发生的位置”。真正的问题往往在更早的地方。
推荐做法:
- 用
headers_sent($file, $line) 主动检查:在 header() 前加一句 if (headers_sent($file, $line)) { die("Headers already sent in $file on line $line"); }
- 用编辑器显示所有空白字符(如 VS Code 开启 “Render Whitespace”),重点检查
<?php 前和 ?> 后
- 用命令行运行脚本:
php -l your_script.php 检查语法,再 php your_script.php | head -n5 看是否开头就有空行或 HTML
- 禁用所有 include/require,逐个恢复,观察何时出错
output_buffering 不是万能解药,但可临时兜底
开启输出缓冲(output_buffering = On 或在脚本开头加 ob_start())确实能让 PHP 把输出暂存内存,延迟发送,从而让 header() 看似“还能用”。
但要注意:
- 它掩盖了根本问题,上线后若缓冲区满或手动
ob_flush() / ob_end_flush(),错误仍会重现
- 某些环境(如 CLI、部分 SAPI)不支持 output buffering
- 如果用了
ob_gzhandler 或自定义回调,可能引入编码或截断风险
- 调试时建议关掉 buffering(
ini_set('output_buffering', '0');),逼自己修复源头
登录跳转、AJAX 返回等典型场景的稳妥写法
比如用户登录成功后重定向:
session_start();
// ... 验证逻辑
if ($valid) {
$_SESSION['user_id'] = $id;
// ✅ 正确:无任何 echo/print,无空行,无 BOM
header('Location: /dashboard.php');
exit; // 必须 exit,否则后续代码仍会执行并可能输出
}
又比如 API 接口返回 JSON:
// ✅ 正确顺序:设置 header → 输出内容 → exit(可选,但推荐)
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'ok']);
exit;
容易踩的坑:
- 在
header() 后忘记 exit 或 die(),导致重定向后继续执行页面渲染逻辑
- 用
include 'footer.php' 在 header() 后,而 footer.php 里有 HTML 输出
- 在类方法中调用
header(),但该方法被其他逻辑提前触发了输出(比如构造函数里写了 echo)
BOM 和空白字符这种问题,改一次就能一劳永逸;靠 ob_start() 顶着,迟早会在某个缓存策略、CDN 或新服务器上崩。真正要盯住的,是每一处 include、每一个模板文件的首尾、以及 IDE 保存时的编码选项。
?><?php 之间有空格或换行(比如模板文件末尾多了一个回车)echo "loaded";)error_reporting(E_ALL) 开启后,未捕获的 Notice/Warning 也会作为文本输出,破坏 header 时机in /path/to/file.php on line 12——但这只是 header() 被调用的位置,不是“输出发生的位置”。真正的问题往往在更早的地方。
推荐做法:
- 用
headers_sent($file, $line)主动检查:在header()前加一句if (headers_sent($file, $line)) { die("Headers already sent in $file on line $line"); } - 用编辑器显示所有空白字符(如 VS Code 开启 “Render Whitespace”),重点检查
<?php前和?>后 - 用命令行运行脚本:
php -l your_script.php检查语法,再php your_script.php | head -n5看是否开头就有空行或 HTML - 禁用所有 include/require,逐个恢复,观察何时出错
output_buffering 不是万能解药,但可临时兜底
开启输出缓冲(output_buffering = On 或在脚本开头加 ob_start())确实能让 PHP 把输出暂存内存,延迟发送,从而让 header() 看似“还能用”。
但要注意:
- 它掩盖了根本问题,上线后若缓冲区满或手动
ob_flush() / ob_end_flush(),错误仍会重现
- 某些环境(如 CLI、部分 SAPI)不支持 output buffering
- 如果用了
ob_gzhandler 或自定义回调,可能引入编码或截断风险
- 调试时建议关掉 buffering(
ini_set('output_buffering', '0');),逼自己修复源头
登录跳转、AJAX 返回等典型场景的稳妥写法
比如用户登录成功后重定向:
session_start();
// ... 验证逻辑
if ($valid) {
$_SESSION['user_id'] = $id;
// ✅ 正确:无任何 echo/print,无空行,无 BOM
header('Location: /dashboard.php');
exit; // 必须 exit,否则后续代码仍会执行并可能输出
}
又比如 API 接口返回 JSON:
// ✅ 正确顺序:设置 header → 输出内容 → exit(可选,但推荐)
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'ok']);
exit;
容易踩的坑:
- 在
header() 后忘记 exit 或 die(),导致重定向后继续执行页面渲染逻辑
- 用
include 'footer.php' 在 header() 后,而 footer.php 里有 HTML 输出
- 在类方法中调用
header(),但该方法被其他逻辑提前触发了输出(比如构造函数里写了 echo)
BOM 和空白字符这种问题,改一次就能一劳永逸;靠 ob_start() 顶着,迟早会在某个缓存策略、CDN 或新服务器上崩。真正要盯住的,是每一处 include、每一个模板文件的首尾、以及 IDE 保存时的编码选项。
ob_flush() / ob_end_flush(),错误仍会重现ob_gzhandler 或自定义回调,可能引入编码或截断风险ini_set('output_buffering', '0');),逼自己修复源头session_start();
// ... 验证逻辑
if ($valid) {
$_SESSION['user_id'] = $id;
// ✅ 正确:无任何 echo/print,无空行,无 BOM
header('Location: /dashboard.php');
exit; // 必须 exit,否则后续代码仍会执行并可能输出
}
又比如 API 接口返回 JSON:
// ✅ 正确顺序:设置 header → 输出内容 → exit(可选,但推荐)
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'ok']);
exit;
容易踩的坑:
- 在
header()后忘记exit或die(),导致重定向后继续执行页面渲染逻辑 - 用
include 'footer.php'在header()后,而 footer.php 里有 HTML 输出 - 在类方法中调用
header(),但该方法被其他逻辑提前触发了输出(比如构造函数里写了echo)
ob_start() 顶着,迟早会在某个缓存策略、CDN 或新服务器上崩。真正要盯住的,是每一处 include、每一个模板文件的首尾、以及 IDE 保存时的编码选项。



















